Reputation: 41
I am currently reading a book security vulnerabilities and have come to the section on stack-based buffer overflows. It gives an example similar to the one that follows.
//overFlowTest.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void main(int argv, char* argv[])
{
int i = 0;
char buffer[4];
strcpy(buffer, argv[1]);
if(i)
{
printf("overwrote i\n");
}
}
When I compile and run the program with an input argument that is longer than the available space allocated for that variable "AAAAA", I get the following as expected (because I overwrote the i variable since it has a numerically larger address (lower in the stack) on the stack than "buffer").
# gcc overFlowTest.c
# ./a.out AAAAA
overwrote buffer
#
But then when I change the order of how the local variables are created, I would think they would get pushed to the stack in the opposite order and the buffer overflow would not work.
//overFlowTest.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void main(int argv, char* argv[])
{
char buffer[4];
int i = 0;
strcpy(buffer, argv[1]);
if(i)
{
printf("overwrote i\n");
}
}
But this does not seem to be the case, as I get the same result.
# gcc overFlowTest.c
# ./a.out AAAAA
overwrote buffer
#
Any ideas on why this is happening?
Upvotes: 3
Views: 112
Reputation: 1183
So, I found some interesting information while exploring this problem.
One, this problem is not reproduced with clang. When I compile the second program, I don't see the print statement as shown here:
$ clang so.c -o so.out
so.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main(int arg, char* argv[])
^
so.c:4:1: note: change return type to 'int'
void main(int arg, char* argv[])
^~~~
int
1 warning generated.
$ clang so2.c -o so2.out
so2.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main(int arg, char* argv[])
^
so2.c:4:1: note: change return type to 'int'
void main(int arg, char* argv[])
^~~~
int
1 warning generated.
$ ./so.out AAAAAAAAAAAAAAAAAAA
overwrote i
$ ./so2.out AAAAAAAAAAAAAAAAAAA
$
However, if we do the same with gcc, we see that they both fail.
$ gcc so.c -o so.exe
$ gcc so2.c -o so2.exe
$ ./so.exe AAAAAAAAAAAAAAAA
overwrote i
$ ./so2.exe AAAAAAAAAAAAAAAA
overwrote i
$
Looking a bit further, let's look at the assembly for these
$ gcc so.c -S -masm=intel
$ gcc so2.c -S -masm=intel
$ diff so.s so2.s
1c1
< .file "so.c"
---
> .file "so2.c"
$
As you can see, the only difference here is the filename (I also tested with all optimizations off, with the same result).
Now, let's try with clang.
$ clang -S -mllvm --x86-asm-syntax=intel so.c
so.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main(int arg, char* argv[])
^
so.c:4:1: note: change return type to 'int'
void main(int arg, char* argv[])
^~~~
int
1 warning generated.
$ clang -S -mllvm --x86-asm-syntax=intel so2.c
so2.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main(int arg, char* argv[])
^
so2.c:4:1: note: change return type to 'int'
void main(int arg, char* argv[])
^~~~
int
1 warning generated.
$ diff so.s so2.s
26c26
< lea rax, qword ptr [rbp - 24]
---
> lea rax, qword ptr [rbp - 20]
29c29
< mov dword ptr [rbp - 20], 0
---
> mov dword ptr [rbp - 24], 0
34c34
< cmp dword ptr [rbp - 20], 0
---
> cmp dword ptr [rbp - 24], 0
$
It looks like clang compiles the two files to different versions; this prevents i from being overwritten.
In conclusion, the reason why these two files produce different output is because the code produces undefined behaviour - the compiler is not bound by any standards and can produce whatever is easiest.
Upvotes: 3