1. Overview

A buffer overflow occurs when a program writes data to a buffer but exceeds the boundaries of the buffer. There are two types of buffer overflows in general: stack-based and heap-based.

In this tutorial, we’ll discuss how to activate and deactivate stack-based buffer overflow checks in Linux using gcc and clang-11.

2. Code Example

We’ll use the following C program, buffer_overflow.c:

#include <string.h>
#include <stdio.h>

void foo(char *str) {
    char buffer[10];
    strcpy(buffer, str);
}

int main(int ac, char **av) {

    printf("Copying data\n");
    foo(av[1});
    printf("Copied data\n");

    return 0;
}

In main(), we just call the foo() function by passing the first argument of the program, av[1], to it and then exit. Additionally, we print debug messages before and after calling foo() using printf().

The foo() function takes a single parameter str of type char*. We copy the string, which the str pointer points to, to the buffer array of type char [10] using strcpy(). If the length of the string that str points is longer than the length of buffer, which is 10, then we have a stack buffer overflow problem.

3. Using gcc

gcc is the standard compiler for most Linux distros. We’ll inspect how to activate and deactivate stack buffer overflow checks using gcc in this section.

Firstly, we’ll inspect the behavior of the program when we compile it using gcc with no additional options. Then, we’ll inspect the behavior when we compile it using the -fstack-protector option of gcc. Finally, we’ll compare the generated assembly code of foo() in both cases.

3.1. The Default Behavior

Let’s build the executable buffer_overflow using gcc:

$ gcc --version
gcc (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc buffer_overflow.c -o buffer_overflow 

The version of gcc is 10.2.1. Having built the executable buffer_overflow, let’s run it:

$ ./buffer_overflow 012345
Copying data
Copied data

We don’t have any problem in this case since the length of the argument, 012345, we pass to buffer_overflow is less than 10.

Now, let’s rerun buffer_overflow with an argument of different length:

$ ./buffer_overflow 012345678901234567890123456789
Copying data
Segmentation fault

This time, the length of the input string is larger than 10, and we have a segmentation fault.

Although the buffer overflow in our example causes the program to crash, this may not always be the case. For example, a buffer overflow may overwrite the return address in the stack. Therefore, the program jumps to the location specified by the new return address. This might lead to executing malicious code put by attackers to the new return address.

Therefore, we may want to abort the program intentionally when we detect a buffer overflow.

3.2. The Behavior When Compiled With -fstack-protector

gcc has an option, -fstack-protector, to check buffer overflows during run-time. When we compile a program using this option, the compiler adds guard variables and control mechanisms to functions with buffers larger than 8 bytes and functions that call alloca(). The compiler initializes the guard variable at the beginning of the function and checks the guard variable while exiting from the function. If the check fails, it prints an error message.

The guard variable is also known as the canary value.

Let’s build buffer_overflow.c using the -fstack-protector option:

$ gcc -fstack-protector buffer_overflow.c -o buffer_overflow.fstack_protector

The name of the executable is buffer_overflow.fstack_protector this time. Let’s run it by passing an argument whose length is larger than 10:

$ ./buffer_overflow.fstack_protector 012345678901234567890123456789
Copying data
*** stack smashing detected ***: terminated
Aborted

The behavior of the spawned process is different now. Although the process still aborts, this abortion is intentional since a stack buffer overflow is detected. Stack smashing is another name for stack buffer overflow.

Therefore, stack protection is off by default in the current version of gcc in Debian 10. We can enable it using the -fstack-protector option, as we’ve just seen.

However, stack protection may be on by default in old or other Linux distros. We can disable it by using the -fno-stack-protector option of gcc.

There are other options of gcc for stack protection. For example, the -fstack-protector-strong option is like the -fstack-protector option, but it also protects against buffer overflows in functions having local array definitions.

Similarly, the -fstack-protector-all option protects all functions against buffer overflow. For example, the compiler generates stack canaries even for an empty function like the following when we use the -fstack-protector-all option:

void bar() {
}

3.3. Comparison of the Assembly Code

We’ll examine the effect of the -fstack-protector option on the assembly code of the foo() function generated by the gcc compiler. We’ll use the objdump command for this purpose.

Let’s first disassemble foo() in buffer_overflow:

$ objdump --no-show-raw-insn --no-addresses --disassemble=foo -j .text buffer_overflow

buffer_overflow:     file format elf64-x86-64


Disassembly of section .text:

<foo>:
    push   %rbp
    mov    %rsp,%rbp
    sub    $0x20,%rsp
    mov    %rdi,-0x18(%rbp)
    mov    -0x18(%rbp),%rdx
    lea    -0xa(%rbp),%rax
    mov    %rdx,%rsi
    mov    %rax,%rdi
    callq  <strcpy@plt>
    nop
    leaveq 
    retq

We use the –no-show-raw-insn and –no-addresses options of objdump to have a clean output. The first option doesn’t print the instruction bytes while disassembling instructions. The second one, on the other hand, doesn’t print addresses on each line or for symbols and relocation offsets. Therefore, we just have the assembly code in the output.

The –disassemble=foo option is for displaying only the generated assembly code of foo(). Additionally, the -j .text option displays information only for the .text section.

Now, let’s disassemble foo() in buffer_overflow.fstack-protector:

$ objdump --no-show-raw-insn --no-addresses --disassemble=foo -j .text buffer_overflow.fstack-protector

buffer_overflow.fstack-protector:     file format elf64-x86-64


Disassembly of section .text:

<foo>:
        push   %rbp
        mov    %rsp,%rbp
        sub    $0x30,%rsp
        mov    %rdi,-0x28(%rbp)
        mov    %fs:0x28,%rax
        mov    %rax,-0x8(%rbp)
        xor    %eax,%eax
        mov    -0x28(%rbp),%rdx
        lea    -0x12(%rbp),%rax
        mov    %rdx,%rsi
        mov    %rax,%rdi
        callq  <strcpy@plt>
        nop
        mov    -0x8(%rbp),%rax
        sub    %fs:0x28,%rax
        je     <foo+0x43>
        callq  <__stack_chk_fail@plt>
        leaveq
        retq

Most of the assembly code of the foo() functions in buffer_overflow and buffer_overflow.fstack-protector have common parts.

Let’s focus on the differences:

        mov    %fs:0x28,%rax
        mov    %rax,-0x8(%rbp)
        xor    %eax,%eax

This code copies a random secret value at %fs:0x28 into the stack. The copied value in the stack at -0x8(%rbp) is the canary value. The offset 0x28 from the fs segment register points to a memory region outside of the stack.

Now, let’s also inspect the additional code before the function foo() returns:

        mov    -0x8(%rbp),%rax
        sub    %fs:0x28,%rax
        je     <foo+0x43>
        callq  <__stack_chk_fail@plt>

This code reads the canary value in the stack and compares the value with the secret value. If they’re the same, this means that no buffer overflow exists. However, if they’re different, this means that the canary value has been overridden because of a buffer overflow. In this case, the program calls __stack_chk_fail, which terminates the program after printing an error message.

Therefore, compiling a program using the -fstack-protector option of gcc generates extra code for detecting stack buffer overflows.

4. Using clang-11

clang-11 is another popular compiler that supports most of the options of gcc. We’ll see that clang-11 supports the same options as gcc for detecting stack buffer overflows.

4.1. The Default Behavior

Let’s build the executable buffer_overflow.clang using clang-11:

$ clang-11 –version
Debian clang version 11.0.1-2
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ clang-11 buffer_overflow.c -o buffer_overflow.clang 

The version of clang-11 is 11.0.1-2. Having built the executable buffer_overflow.clang, let’s run it with a long argument:

$ ./buffer_overflow.clang 012345678901234567890123456789
Copying data
Segmentation fault

The behavior of the program is the same as the one built using gcc without the -fstack-protector option.

4.2. The Behavior When Compiled With -fstack-protector

clang-11 also supports the -fstack-protector option. Let’s build the program using this option:

$ clang-11 -fstack-protector buffer_overflow.c -o buffer_overflow.clang.fstack_protector

The output of the executable is buffer_overflow.clang.fstack_protector this time. Let’s run it by passing an argument whose length is larger than 10:

$ ./buffer_overflow.clang.fstack_protector 012345678901234567890123456789
Copying data
*** stack smashing detected ***: terminated
Aborted

The output is the same as the output of the one built using gcc with the -fstack-protector option.

Therefore, stack protection is off by default in the current version of clang-11 in Debian 10. We can enable it using the -fstack-protector option.

clang-11 also supports the other options like -fno-stack-protector, -fstack-protector-strong and -fstack-protector-all.

5. Conclusion

In this article, we discussed how to activate and deactivate stack buffer overflow checks.

Firstly, we examined the behavior of a program when a buffer overflow occurs. The program crashed in our case. But we learned that this may not always be the case.

Then, we compiled the program with the -fstack-protector option of gcc. We saw that this option enabled stack buffer overflow checks.

We also compared the assembly code when the stack buffer overflow check is enabled and disabled.

Finally, we saw that clang-11 also supports the same options for stack buffer overflow checking as gcc supports.