Andy Woerpel
Andy Woerpel

Reputation: 41

Local variable position on stack not changing

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

Answers (1)

Kevin W.
Kevin W.

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

Related Questions