DEV Community

ddupard
ddupard

Posted on

X86_64 From Source Code to Binary: What Does GCC Actually Do? part 3

Having explained how GCC uses the System V AMD64 ABI convention for passing parameters , how GCC uses the stack to manage local variables and how GCC returns values from a function, it's time to explain some of the options GCC has to prevent the buffer overflow attack

To illustrate the buffer overflow protection mechanism, I will use the following program:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 64


void test1() {
    char *t1 = "test1" ; 
    printf("%s\n",t1) ;
} 

void test2() {
    char *buffer1 = "test2\n" ;
    char *buffer2[10]  ;

    strcpy(buffer2[0],buffer1) ;
    printf("%s",buffer2[0]) ;
}



/**
 * handle_client: Processes incoming connections.
 * This function contains a deliberate stack-based buffer overflow 
 * vulnerability for educational purposes.
 */


void handle_client(int client_socket) {
    char buffer[BUFFER_SIZE];
    char request[256];

    // Read the incoming request from the client
    int bytes_received = recv(client_socket, request, sizeof(request) - 1, 0);
    if (bytes_received > 0) {
        request[bytes_received] = '\0';

        /* * THE VULNERABILITY: 
         * strcpy() does not perform bounds checking. If 'request' 
         * exceeds 64 bytes, it will overflow 'buffer' and overwrite 
         * critical data on the stack, including the saved return address.
         */
        strcpy(buffer, request);
        printf("Received: %s\n", buffer);

        test1() ;
        test2() ;



    }
    close(client_socket);
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address = {AF_INET, htons(PORT), INADDR_ANY};

    // Bind the socket to the port and start listening
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        int client_socket = accept(server_fd, NULL, NULL);
        handle_client(client_socket);
    }
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

This program will be compiled with GCC and the binary generated will be disassembled by objdump using the following commands

if [ ! -f  "$OUT/gcc_3/server.objdump_clean" ]; then
    gcc -g -fno-stack-protector -z execstack -no-pie  -o "$OUT/gcc_3/server" "$SRC/buffer_overflow/C/server.c"
    objdump -M intel --show-raw-insn -d "$OUT/gcc_3/server" > "$OUT/gcc_3/server.objdump"
    python3 "$ATP/clean_objdump.py" "$OUT/gcc_3/server.objdump" > "$OUT/gcc_3/server.objdump_clean"
fi

if [ ! -f  "$OUT/gcc_3/server_sp.objdump_clean" ]; then
    gcc -g -fstack-protector -z execstack -no-pie  -o "$OUT/gcc_3/server_sp" "$SRC/buffer_overflow/C/server.c"
    objdump -M intel --show-raw-insn -d "$OUT/gcc_3/server_sp" > "$OUT/gcc_3/server_sp.objdump"
    python3 "$ATP/clean_objdump.py" "$OUT/gcc_3/server_sp.objdump" > "$OUT/gcc_3/server_sp.objdump_clean"
fi

if [ ! -f  "$OUT/gcc_3/server_sps.objdump_clean" ]; then
    gcc -g -fstack-protector-strong -z execstack -no-pie  -o "$OUT/gcc_3/server_sps" "$SRC/buffer_overflow/C/server.c"
    objdump -M intel --show-raw-insn -d "$OUT/gcc_3/server_sps" > "$OUT/gcc_3/server_sps.objdump"
    python3 "$ATP/clean_objdump.py" "$OUT/gcc_3/server_sps.objdump" > "$OUT/gcc_3/server_sps.objdump_clean"
fi

if [ ! -f  "$OUT/gcc_3/server_spa.objdump_clean" ]; then
    gcc -g -fstack-protector-all -z execstack -no-pie  -o "$OUT/gcc_3/server_spa" "$SRC/buffer_overflow/C/server.c"
    objdump -M intel --show-raw-insn -d "$OUT/gcc_3/server_spa" > "$OUT/gcc_3/server_spa.objdump"
    python3 "$ATP/clean_objdump.py" "$OUT/gcc_3/server_spa.objdump" > "$OUT/gcc_3/server_spa.objdump_clean"
fi

if [ ! -f  "$OUT/gcc_3/server_default.objdump_clean" ]; then
    gcc -g -z execstack -no-pie  -o "$OUT/gcc_3/server_default" "$SRC/buffer_overflow/C/server.c"
    objdump -M intel --show-raw-insn -d "$OUT/gcc_3/server_default" > "$OUT/gcc_3/server_default.objdump"
    python3 "$ATP/clean_objdump.py" "$OUT/gcc_3/server_default.objdump" > "$OUT/gcc_3/server_default.objdump_clean"
fi

Enter fullscreen mode Exit fullscreen mode

The option ( -M intel ) forces the intel convention in the disasssembling process. I prefer the intel convetion over the ATT convention.

1. The stack based buffer overflow mechanism

A "buffer overflow" occurs when a program writes more data to a buffer than its allocated capacity, overwriting adjacent memory and resulting in the corruption of adjacent memory β€”most notably the saved return address on the stack. It can happen when using functions like strcpy, memcpy, ..
Buffers can be allocated on stack so we use the term stack based buffer overflow or on heap and we use the term Heap buffer overflow.

For example in our C program, we copy the information found in request (256 bytes) in a buffer containing only 64 bytes using the following line
strcpy(buffer, request);
This results to a corruption of the stack which normally contains the return address of the function, so an attacker by modifying the return address can change the execution flow of the program and execute an arbitrary code.

Since this buffer is allocated on stack using a local variable, we are creating a stack based buffer overflow.

GCC default options partially protect programs from it but we will see that some stack based buffer overflows are still found each year even in programs where we do not expect to find them.

but let's start with GCC options

2. -fno-stack-protector

This option removes the default GCC mechanism of protecting programs from stack based buffer overflows. This is only useful to study the stack based buffer overflow mechanism and I don't see any other reason to have this option set when compiling a program.

0000000000401256 <test1>:
401256:  endbr64
40125a:  push   rbp
40125b:  mov    rbp,rsp
40125e:  sub    rsp,0x10
401262:  lea    rax,[rip+0xd9f]        # 402008 <_IO_stdin_used+0x8>
401269:  mov    QWORD PTR [rbp-0x8],rax
40126d:  lea    rax,[rip+0xffffffffffffffe2]        # 401256 <test1>
401274:  mov    rdi,rax
401277:  call   4010f0 <puts@plt>
40127c:  nop
40127d:  leave
40127e:  ret

000000000040127f <test2>:
40127f:  endbr64
401283:  push   rbp
401284:  mov    rbp,rsp
401287:  sub    rsp,0x10
40128b:  lea    rax,[rip+0xd7c]        # 40200e <_IO_stdin_used+0xe>
401292:  mov    BYTE PTR [rbp-0x1],al
401295:  lea    rax,[rip+0xd78]        # 402014 <_IO_stdin_used+0x14>
40129c:  mov    BYTE PTR [rbp-0x2],al
40129f:  movsx  edx,BYTE PTR [rbp-0x2]
4012a3:  movsx  eax,BYTE PTR [rbp-0x1]
4012a7:  mov    esi,eax
4012a9:  lea    rax,[rip+0xd66]        # 402016 <_IO_stdin_used+0x16>
4012b0:  mov    rdi,rax
4012b3:  mov    eax,0x0
4012b8:  call   401110 <printf@plt>
4012bd:  nop
4012be:  leave
4012bf:  ret

00000000004012c0 <handle_client>:
4012c0:  endbr64
4012c4:  push   rbp
4012c5:  mov    rbp,rsp
4012c8:  sub    rsp,0x160
4012cf:  mov    DWORD PTR [rbp-0x154],edi
4012d5:  lea    rsi,[rbp-0x150]
4012dc:  mov    eax,DWORD PTR [rbp-0x154]
4012e2:  mov    ecx,0x0
4012e7:  mov    edx,0xff
4012ec:  mov    edi,eax
4012ee:  call   4010d0 <recv@plt>
4012f3:  mov    DWORD PTR [rbp-0x4],eax
4012f6:  cmp    DWORD PTR [rbp-0x4],0x0
4012fa:  jle    40134e <handle_client+0x8e>
4012fc:  mov    eax,DWORD PTR [rbp-0x4]
4012ff:  cdqe
401301:  mov    BYTE PTR [rbp+rax*1-0x150],0x0
401309:  lea    rdx,[rbp-0x150]
401310:  lea    rax,[rbp-0x50]
401314:  mov    rsi,rdx
401317:  mov    rdi,rax
40131a:  call   4010e0 <strcpy@plt>
40131f:  lea    rax,[rbp-0x50]
401323:  mov    rsi,rax
401326:  lea    rax,[rip+0xcee]        # 40201b <_IO_stdin_used+0x1b>
40132d:  mov    rdi,rax
401330:  mov    eax,0x0
401335:  call   401110 <printf@plt>
40133a:  mov    eax,0x0
40133f:  call   401256 <test1>
401344:  mov    eax,0x0
401349:  call   40127f <test2>
40134e:  mov    eax,DWORD PTR [rbp-0x154]
401354:  mov    edi,eax
401356:  call   401120 <close@plt>
40135b:  nop
40135c:  leave
40135d:  ret

000000000040135e <main>:
40135e:  endbr64
401362:  push   rbp
401363:  mov    rbp,rsp
401366:  sub    rsp,0x20
40136a:  mov    edx,0x0
40136f:  mov    esi,0x1
401374:  mov    edi,0x2
401379:  call   401160 <socket@plt>
40137e:  mov    DWORD PTR [rbp-0x4],eax
401381:  mov    QWORD PTR [rbp-0x20],0x0
401389:  mov    QWORD PTR [rbp-0x18],0x0
401391:  mov    WORD PTR [rbp-0x20],0x2
401397:  mov    edi,0x1f90
40139c:  call   401100 <htons@plt>
4013a1:  mov    WORD PTR [rbp-0x1e],ax
4013a5:  lea    rcx,[rbp-0x20]
4013a9:  mov    eax,DWORD PTR [rbp-0x4]
4013ac:  mov    edx,0x10
4013b1:  mov    rsi,rcx
4013b4:  mov    edi,eax
4013b6:  call   401140 <bind@plt>
4013bb:  mov    eax,DWORD PTR [rbp-0x4]
4013be:  mov    esi,0x3
4013c3:  mov    edi,eax
4013c5:  call   401130 <listen@plt>
4013ca:  mov    esi,0x1f90
4013cf:  lea    rax,[rip+0xc5a]        # 402030 <_IO_stdin_used+0x30>
4013d6:  mov    rdi,rax
4013d9:  mov    eax,0x0
4013de:  call   401110 <printf@plt>
4013e3:  mov    eax,DWORD PTR [rbp-0x4]
4013e6:  mov    edx,0x0
4013eb:  mov    esi,0x0
4013f0:  mov    edi,eax
4013f2:  call   401150 <accept@plt>
4013f7:  mov    DWORD PTR [rbp-0x8],eax
4013fa:  mov    eax,DWORD PTR [rbp-0x8]
4013fd:  mov    edi,eax
4013ff:  call   4012c0 <handle_client>
401404:  nop
401405:  jmp    4013e3 <main+0x85>
Enter fullscreen mode Exit fullscreen mode

let notice several things
First the size of the local memory allocated on the stack (0x160) for the handle_client function
4012c8: sub rsp,0x160

3. -fstack-protector

Let's see what the man page says about the option

-fstack-protector
Emit extra code to check for buffer overflows, such as stack
smashing attacks. This is done by adding a guard variable to
functions with vulnerable objects. This includes functions
that call "alloca", and functions with buffers larger than 8
bytes. The guards are initialized when a function is entered
and then checked when the function exits. If a guard check
fails, an error message is printed and the program exits

0000000000401276 <test1>:
401276:  endbr64
40127a:  push   rbp
40127b:  mov    rbp,rsp
40127e:  sub    rsp,0x10
401282:  lea    rax,[rip+0xd7f]        # 402008 <_IO_stdin_used+0x8>
401289:  mov    QWORD PTR [rbp-0x8],rax
40128d:  lea    rax,[rip+0xffffffffffffffe2]        # 401276 <test1>
401294:  mov    rdi,rax
401297:  call   401100 <puts@plt>
40129c:  nop
40129d:  leave
40129e:  ret

000000000040129f <test2>:
40129f:  endbr64
4012a3:  push   rbp
4012a4:  mov    rbp,rsp
4012a7:  sub    rsp,0x10
4012ab:  lea    rax,[rip+0xd5c]        # 40200e <_IO_stdin_used+0xe>
4012b2:  mov    BYTE PTR [rbp-0x2],al
4012b5:  lea    rax,[rip+0xd58]        # 402014 <_IO_stdin_used+0x14>
4012bc:  mov    BYTE PTR [rbp-0x1],al
4012bf:  movsx  edx,BYTE PTR [rbp-0x1]
4012c3:  movsx  eax,BYTE PTR [rbp-0x2]
4012c7:  mov    esi,eax
4012c9:  lea    rax,[rip+0xd46]        # 402016 <_IO_stdin_used+0x16>
4012d0:  mov    rdi,rax
4012d3:  mov    eax,0x0
4012d8:  call   401130 <printf@plt>
4012dd:  nop
4012de:  leave
4012df:  ret

00000000004012e0 <handle_client>:
4012e0:  endbr64
4012e4:  push   rbp
4012e5:  mov    rbp,rsp
4012e8:  sub    rsp,0x170
4012ef:  mov    DWORD PTR [rbp-0x164],edi
4012f5:  mov    rax,QWORD PTR fs:0x28
4012fe:  mov    QWORD PTR [rbp-0x8],rax
401302:  xor    eax,eax
401304:  lea    rsi,[rbp-0x110]
40130b:  mov    eax,DWORD PTR [rbp-0x164]
401311:  mov    ecx,0x0
401316:  mov    edx,0xff
40131b:  mov    edi,eax
40131d:  call   4010e0 <recv@plt>
401322:  mov    DWORD PTR [rbp-0x154],eax
401328:  cmp    DWORD PTR [rbp-0x154],0x0
40132f:  jle    40138c <handle_client+0xac>
401331:  mov    eax,DWORD PTR [rbp-0x154]
401337:  cdqe
401339:  mov    BYTE PTR [rbp+rax*1-0x110],0x0
401341:  lea    rdx,[rbp-0x110]
401348:  lea    rax,[rbp-0x150]
40134f:  mov    rsi,rdx
401352:  mov    rdi,rax
401355:  call   4010f0 <strcpy@plt>
40135a:  lea    rax,[rbp-0x150]
401361:  mov    rsi,rax
401364:  lea    rax,[rip+0xcb0]        # 40201b <_IO_stdin_used+0x1b>
40136b:  mov    rdi,rax
40136e:  mov    eax,0x0
401373:  call   401130 <printf@plt>
401378:  mov    eax,0x0
40137d:  call   401276 <test1>
401382:  mov    eax,0x0
401387:  call   40129f <test2>
40138c:  mov    eax,DWORD PTR [rbp-0x164]
401392:  mov    edi,eax
401394:  call   401140 <close@plt>
401399:  nop
40139a:  mov    rax,QWORD PTR [rbp-0x8]
40139e:  sub    rax,QWORD PTR fs:0x28
4013a7:  je     4013ae <handle_client+0xce>
4013a9:  call   401110 <__stack_chk_fail@plt>
4013ae:  leave
4013af:  ret

Enter fullscreen mode Exit fullscreen mode

In this assembly , one can see that the option -fstack-protector has modified several things,

First (step 1) the size of the local memory allocated on the stack (0x170)
4012e8: sub rsp,0x170

Second (step 2) at the begining of the handle_function, right after the local memory allocation, GCC adds the following 3 lines
4012f5: mov rax,QWORD PTR fs:0x28
4012fe: mov QWORD PTR [rbp-0x8],rax
401302: xor eax,eax

Third (step 3) GCC adds some lines at the end of the handle_client function
40139a: mov rax,QWORD PTR [rbp-0x8]
40139e: sub rax,QWORD PTR fs:0x28
4013a7: je 4013ae
4013a9: call 401110 <__stack_chk_fail@plt>

  1. So how this mechanism works ?
    The program at execution is going to add a random list of bytes (called the stack canary, stack cookie ) at the end of the stack allocated.
    In order to do that, it has to allocate some more space (step 1) on the stack than when there is no protection.
    Then it copies 8 bytes from a specific location ( fs:0x28 see step 2) to the end of the stack ( [rbp-0x8] )

    Then at the end of the function, the program checks that the stack canary has not been corrupted else it calls __stack_chk_fail@plt

  2. Understanding mov rax, QWORD PTR fs:0x28
    This instruction is used to ensure the integrity of the program's execution.
    This instruction copies a value located in a special memory segment ( fs ) into the rax register.
    In protected mode, the fs segment register points to a thread-specific memory structure called Thread Local Storage (TLS) or the tcbhead_t (Thread Control Block). The 0x28 offset: This is the specific location where the "Stack Canary" is stored.

  3. Who generates the value at fs:0x28?
    The execution environment, specifically the dynamic linker (/usr/lib64/ld-linux-x86-64.so.2 or /usr/lib32/ld-linux.so.2).

Here is the chain of responsibility:

a. The role of the Dynamic Linker
When you run a program on Linux, before the main function executes, the kernel loads the binary into memory and then hands control to the dynamic linker. This is the component that sets up the process environment.

During this initialization phase, the linker generates a random (or pseudo-random) value using a system call, typically getrandom() or by reading /dev/urandom. This value is then copied into the internal tcbhead_t structure associated with the fs segment. This is precisely when fs:0x28 is populated.

b. Why the linker and not the program?
It is imperative that this value is generated before your program code begins to execute. If the program itself generated the canary, it would need to be protected during that generation, creating a "chicken and egg" problem. By delegating this to the system linker, we ensure that every thread is protected from its very first cycle of execution.

c. Persistence of the value
Once the linker places this value at fs:0x28, it remains read-only (in theory) for the thread for its entire lifetime.

Per process: The canary is generally the same for all threads within a single process.

Per execution: Every time the program starts, the linker generates a new value. This is why even if an attacker discovers the canary for one session, they cannot use it for another; they would have to leak it again.

  1. why the name stack canary ?
    The name comes from the use of canaries in a mine. In mines they used canaries to alert for gas poisoning. If the canary stops singing, it means that you have to get out before you die from gas poisoning.

  2. Finally
    In the example above handle_client is protected but not test1 nor test2

4. fstack-protector-strong

Let's see what the man page says about the option
-fstack-protector-strong
Like -fstack-protector but includes additional functions to be
protected --- those that have local array definitions, or have
references to local frame addresses.

0000000000401276 <test1>:
401276:  endbr64
40127a:  push   rbp
40127b:  mov    rbp,rsp
40127e:  sub    rsp,0x10
401282:  lea    rax,[rip+0xd7f]        # 402008 <_IO_stdin_used+0x8>
401289:  mov    QWORD PTR [rbp-0x8],rax
40128d:  mov    rax,QWORD PTR [rbp-0x8]
401291:  mov    rdi,rax
401294:  call   401100 <puts@plt>
401299:  nop
40129a:  leave
40129b:  ret

000000000040129c <test2>:
40129c:  endbr64
4012a0:  push   rbp
4012a1:  mov    rbp,rsp
4012a4:  sub    rsp,0x70
4012a8:  mov    rax,QWORD PTR fs:0x28
4012b1:  mov    QWORD PTR [rbp-0x8],rax
4012b5:  xor    eax,eax
4012b7:  lea    rax,[rip+0xd50]        # 40200e <_IO_stdin_used+0xe>
4012be:  mov    BYTE PTR [rbp-0x61],al
4012c1:  movsx  rax,BYTE PTR [rbp-0x61]
4012c6:  mov    rdx,rax
4012c9:  mov    rax,QWORD PTR [rbp-0x60]
4012cd:  mov    rsi,rdx
4012d0:  mov    rdi,rax
4012d3:  call   4010f0 <strcpy@plt>
4012d8:  mov    rax,QWORD PTR [rbp-0x60]
4012dc:  mov    rsi,rax
4012df:  lea    rax,[rip+0xd2f]        # 402015 <_IO_stdin_used+0x15>
4012e6:  mov    rdi,rax
4012e9:  mov    eax,0x0
4012ee:  call   401130 <printf@plt>
4012f3:  nop
4012f4:  mov    rax,QWORD PTR [rbp-0x8]
4012f8:  sub    rax,QWORD PTR fs:0x28
401301:  je     401308 <test2+0x6c>
401303:  call   401110 <__stack_chk_fail@plt>
401308:  leave
401309:  ret

000000000040130a <handle_client>:
40130a:  endbr64
40130e:  push   rbp
40130f:  mov    rbp,rsp
401312:  sub    rsp,0x170
401319:  mov    DWORD PTR [rbp-0x164],edi
40131f:  mov    rax,QWORD PTR fs:0x28
401328:  mov    QWORD PTR [rbp-0x8],rax
40132c:  xor    eax,eax
40132e:  lea    rsi,[rbp-0x110]
401335:  mov    eax,DWORD PTR [rbp-0x164]
40133b:  mov    ecx,0x0
401340:  mov    edx,0xff
401345:  mov    edi,eax
401347:  call   4010e0 <recv@plt>
40134c:  mov    DWORD PTR [rbp-0x154],eax
401352:  cmp    DWORD PTR [rbp-0x154],0x0
401359:  jle    4013b6 <handle_client+0xac>
40135b:  mov    eax,DWORD PTR [rbp-0x154]
401361:  cdqe
401363:  mov    BYTE PTR [rbp+rax*1-0x110],0x0
40136b:  lea    rdx,[rbp-0x110]
401372:  lea    rax,[rbp-0x150]
401379:  mov    rsi,rdx
40137c:  mov    rdi,rax
40137f:  call   4010f0 <strcpy@plt>
401384:  lea    rax,[rbp-0x150]
40138b:  mov    rsi,rax
40138e:  lea    rax,[rip+0xc83]        # 402018 <_IO_stdin_used+0x18>
401395:  mov    rdi,rax
401398:  mov    eax,0x0
40139d:  call   401130 <printf@plt>
4013a2:  mov    eax,0x0
4013a7:  call   401276 <test1>
4013ac:  mov    eax,0x0
4013b1:  call   40129c <test2>
4013b6:  mov    eax,DWORD PTR [rbp-0x164]
4013bc:  mov    edi,eax
4013be:  call   401140 <close@plt>
4013c3:  nop
4013c4:  mov    rax,QWORD PTR [rbp-0x8]
4013c8:  sub    rax,QWORD PTR fs:0x28
4013d1:  je     4013d8 <handle_client+0xce>
4013d3:  call   401110 <__stack_chk_fail@plt>
4013d8:  leave
4013d9:  ret

Enter fullscreen mode Exit fullscreen mode

As you can see here, handle_client , test2 are protected but not test1

5. fstack-protector-all

Let's see what the man page says about the option
-fstack-protector-all
Like -fstack-protector except that all functions are
protected.

0000000000401276 <test1>:
401276:  endbr64
40127a:  push   rbp
40127b:  mov    rbp,rsp
40127e:  sub    rsp,0x10
401282:  mov    rax,QWORD PTR fs:0x28
40128b:  mov    QWORD PTR [rbp-0x8],rax
40128f:  xor    eax,eax
401291:  lea    rax,[rip+0xd70]        # 402008 <_IO_stdin_used+0x8>
401298:  mov    QWORD PTR [rbp-0x10],rax
40129c:  mov    rax,QWORD PTR [rbp-0x10]
4012a0:  mov    rdi,rax
4012a3:  call   401100 <puts@plt>
4012a8:  nop
4012a9:  mov    rax,QWORD PTR [rbp-0x8]
4012ad:  sub    rax,QWORD PTR fs:0x28
4012b6:  je     4012bd <test1+0x47>
4012b8:  call   401110 <__stack_chk_fail@plt>
4012bd:  leave
4012be:  ret

00000000004012bf <test2>:
4012bf:  endbr64
4012c3:  push   rbp
4012c4:  mov    rbp,rsp
4012c7:  sub    rsp,0x70
4012cb:  mov    rax,QWORD PTR fs:0x28
4012d4:  mov    QWORD PTR [rbp-0x8],rax
4012d8:  xor    eax,eax
4012da:  lea    rax,[rip+0xd2d]        # 40200e <_IO_stdin_used+0xe>
4012e1:  mov    BYTE PTR [rbp-0x61],al
4012e4:  movsx  rax,BYTE PTR [rbp-0x61]
4012e9:  mov    rdx,rax
4012ec:  mov    rax,QWORD PTR [rbp-0x60]
4012f0:  mov    rsi,rdx
4012f3:  mov    rdi,rax
4012f6:  call   4010f0 <strcpy@plt>
4012fb:  movsx  eax,BYTE PTR [rbp-0x61]
4012ff:  mov    esi,eax
401301:  lea    rax,[rip+0xd0d]        # 402015 <_IO_stdin_used+0x15>
401308:  mov    rdi,rax
40130b:  mov    eax,0x0
401310:  call   401130 <printf@plt>
401315:  nop
401316:  mov    rax,QWORD PTR [rbp-0x8]
40131a:  sub    rax,QWORD PTR fs:0x28
401323:  je     40132a <test2+0x6b>
401325:  call   401110 <__stack_chk_fail@plt>
40132a:  leave
40132b:  ret

000000000040132c <handle_client>:
40132c:  endbr64
401330:  push   rbp
401331:  mov    rbp,rsp
401334:  sub    rsp,0x170
40133b:  mov    DWORD PTR [rbp-0x164],edi
401341:  mov    rax,QWORD PTR fs:0x28
40134a:  mov    QWORD PTR [rbp-0x8],rax
40134e:  xor    eax,eax
401350:  lea    rsi,[rbp-0x110]
401357:  mov    eax,DWORD PTR [rbp-0x164]
40135d:  mov    ecx,0x0
401362:  mov    edx,0xff
401367:  mov    edi,eax
401369:  call   4010e0 <recv@plt>
40136e:  mov    DWORD PTR [rbp-0x154],eax
401374:  cmp    DWORD PTR [rbp-0x154],0x0
40137b:  jle    4013d8 <handle_client+0xac>
40137d:  mov    eax,DWORD PTR [rbp-0x154]
401383:  cdqe
401385:  mov    BYTE PTR [rbp+rax*1-0x110],0x0
40138d:  lea    rdx,[rbp-0x110]
401394:  lea    rax,[rbp-0x150]
40139b:  mov    rsi,rdx
40139e:  mov    rdi,rax
4013a1:  call   4010f0 <strcpy@plt>
4013a6:  lea    rax,[rbp-0x150]
4013ad:  mov    rsi,rax
4013b0:  lea    rax,[rip+0xc63]        # 40201a <_IO_stdin_used+0x1a>
4013b7:  mov    rdi,rax
4013ba:  mov    eax,0x0
4013bf:  call   401130 <printf@plt>
4013c4:  mov    eax,0x0
4013c9:  call   401276 <test1>
4013ce:  mov    eax,0x0
4013d3:  call   4012bf <test2>
4013d8:  mov    eax,DWORD PTR [rbp-0x164]
4013de:  mov    edi,eax
4013e0:  call   401140 <close@plt>
4013e5:  nop
4013e6:  mov    rax,QWORD PTR [rbp-0x8]
4013ea:  sub    rax,QWORD PTR fs:0x28
4013f3:  je     4013fa <handle_client+0xce>
4013f5:  call   401110 <__stack_chk_fail@plt>
4013fa:  leave
4013fb:  ret

Enter fullscreen mode Exit fullscreen mode

As you can see here, handle_client , test2 and test1 are protected

6. Default option

0000000000401276 <test1>:
401276:  endbr64
40127a:  push   rbp
40127b:  mov    rbp,rsp
40127e:  sub    rsp,0x10
401282:  lea    rax,[rip+0xd7f]        # 402008 <_IO_stdin_used+0x8>
401289:  mov    QWORD PTR [rbp-0x8],rax
40128d:  mov    rax,QWORD PTR [rbp-0x8]
401291:  mov    rdi,rax
401294:  call   401100 <puts@plt>
401299:  nop
40129a:  leave
40129b:  ret

000000000040129c <test2>:
40129c:  endbr64
4012a0:  push   rbp
4012a1:  mov    rbp,rsp
4012a4:  sub    rsp,0x70
4012a8:  mov    rax,QWORD PTR fs:0x28
4012b1:  mov    QWORD PTR [rbp-0x8],rax
4012b5:  xor    eax,eax
4012b7:  lea    rax,[rip+0xd50]        # 40200e <_IO_stdin_used+0xe>
4012be:  mov    BYTE PTR [rbp-0x61],al
4012c1:  movsx  rax,BYTE PTR [rbp-0x61]
4012c6:  mov    rdx,rax
4012c9:  mov    rax,QWORD PTR [rbp-0x60]
4012cd:  mov    rsi,rdx
4012d0:  mov    rdi,rax
4012d3:  call   4010f0 <strcpy@plt>
4012d8:  mov    rax,QWORD PTR [rbp-0x60]
4012dc:  mov    rsi,rax
4012df:  lea    rax,[rip+0xd2f]        # 402015 <_IO_stdin_used+0x15>
4012e6:  mov    rdi,rax
4012e9:  mov    eax,0x0
4012ee:  call   401130 <printf@plt>
4012f3:  nop
4012f4:  mov    rax,QWORD PTR [rbp-0x8]
4012f8:  sub    rax,QWORD PTR fs:0x28
401301:  je     401308 <test2+0x6c>
401303:  call   401110 <__stack_chk_fail@plt>
401308:  leave
401309:  ret

000000000040130a <handle_client>:
40130a:  endbr64
40130e:  push   rbp
40130f:  mov    rbp,rsp
401312:  sub    rsp,0x170
401319:  mov    DWORD PTR [rbp-0x164],edi
40131f:  mov    rax,QWORD PTR fs:0x28
401328:  mov    QWORD PTR [rbp-0x8],rax
40132c:  xor    eax,eax
40132e:  lea    rsi,[rbp-0x110]
401335:  mov    eax,DWORD PTR [rbp-0x164]
40133b:  mov    ecx,0x0
401340:  mov    edx,0xff
401345:  mov    edi,eax
401347:  call   4010e0 <recv@plt>
40134c:  mov    DWORD PTR [rbp-0x154],eax
401352:  cmp    DWORD PTR [rbp-0x154],0x0
401359:  jle    4013b6 <handle_client+0xac>
40135b:  mov    eax,DWORD PTR [rbp-0x154]
401361:  cdqe
401363:  mov    BYTE PTR [rbp+rax*1-0x110],0x0
40136b:  lea    rdx,[rbp-0x110]
401372:  lea    rax,[rbp-0x150]
401379:  mov    rsi,rdx
40137c:  mov    rdi,rax
40137f:  call   4010f0 <strcpy@plt>
401384:  lea    rax,[rbp-0x150]
40138b:  mov    rsi,rax
40138e:  lea    rax,[rip+0xc83]        # 402018 <_IO_stdin_used+0x18>
401395:  mov    rdi,rax
401398:  mov    eax,0x0
40139d:  call   401130 <printf@plt>
4013a2:  mov    eax,0x0
4013a7:  call   401276 <test1>
4013ac:  mov    eax,0x0
4013b1:  call   40129c <test2>
4013b6:  mov    eax,DWORD PTR [rbp-0x164]
4013bc:  mov    edi,eax
4013be:  call   401140 <close@plt>
4013c3:  nop
4013c4:  mov    rax,QWORD PTR [rbp-0x8]
4013c8:  sub    rax,QWORD PTR fs:0x28
4013d1:  je     4013d8 <handle_client+0xce>
4013d3:  call   401110 <__stack_chk_fail@plt>
4013d8:  leave
4013d9:  ret
Enter fullscreen mode Exit fullscreen mode

As we can see above, the default GCC option seems to correspond to -fstack-protector-strong

7. Some history

The -fstack-protector option was introduced into GCC in 2005, specifically with the release of GCC 4.1.

Pre-2005: Before this native GCC integration, developers who wanted stack protection had to rely on a patch for GCC called "StackGuard" (originally developed by Crispin Cowan and his team at the Oregon Graduate Institute in the late 1990s). StackGuard was the pioneering implementation that popularized the "canary" concept.

2005 (GCC 4.1): GCC officially integrated the functionality, making it a standard compiler feature rather than a third-party patch. This was a major milestone for security because it made hardening binaries accessible to the entire open-source ecosystem without requiring custom compiler builds.

Evolution (GCC 4.9+): While the original -fstack-protector existed in 4.1, GCC later introduced more granular levels to balance security and performance:

-fstack-protector-strong (added in GCC 4.9, 2014): Protects functions that have local arrays or references to local stack variables, providing a better middle ground.

-fstack-protector-all: The "paranoid" mode that protects every single function, regardless of its local variables.

8. The situation now

With the -fstack-protector-strong option activated by default, we should not find stack buffer overflows in the CVE database. However we still find a lot of them.

if we search for "Stack-based Buffer Overflow Remote Code Execution" we still find 437 CVE like the following one

Updated: 2026-03-19

Published: 2026-02-02
Title: Libsoup: stack-based buffer overflow in libsoup multipart response parsingmultipart http response

Description
A flaw was found in libsoup. This stack-based buffer overflow vulnerability occurs during the parsing of multipart HTTP responses due to an incorrect length calculation. A remote attacker can exploit this by sending a specially crafted multipart HTTP response, which can lead to memory corruption. This issue may result in application crashes or arbitrary code execution in applications that process untrusted server responses, and it does not require authentication or user interaction.

if we search for "Stack-based Buffer Overflow" we find 5809 CVE like the following one

For example

Updated: 2026-06-29

Published: 2026-05-20
Title: Libsolv: stack-based buffer overflow in libsolv's debian metadata parser when handling sha384/sha512 checksums

Description
A flaw was found in libsolv. This stack-based buffer overflow vulnerability occurs in libsolv's Debian metadata parser when processing specially crafted Debian repository metadata. An attacker could exploit this by providing malicious SHA384 or SHA512 checksum tags, leading to memory corruption and a denial of service (DoS) in the affected system.

Even when a function is hardened with a stack canary, it remains blind to vulnerabilities occurring within the heap or logic errors that corrupt data pointers before the canary check is ever reached

if we search for "Buffer Overflow" we find 22018 CVE most of them being heap buffer overflows

9. Why so ?

  1. The "Non-Stack" Overflows The canary only protects the Return Address on the stack. It does nothing to stop overflows that target other critical data structures:

Heap Overflows: If the buffer is allocated via malloc, the canary is not involved. Attackers target metadata (like chunk headers in glibc) or function pointers stored in the heap to hijack execution.

Global Data Overflows: Overwriting global variables, pointers, or flags in the .data or .bss segments doesn't trigger the stack canary.

Struct Overwriting: If you have two local variables (e.g., a buffer followed by a function pointer) on the stack, an attacker can overflow the buffer to overwrite the pointer before reaching the canary. The function will use the corrupted pointer, and the program will never reach the "canary check" epilogue.

  1. The Legacy Burden (The "Unprotected" World) Millions of lines of C/C++ code: Much of the critical infrastructure (drivers, legacy embedded systems, older enterprise software) was written long before 2014. Migrating this code to compile with modern security flags is a massive, risky, and often prohibitively expensive task.

Non-GCC Compilers: Many specialized environments (embedded firmware, proprietary real-time operating systems) use custom or older compilers that do not have stack-protector-strong implemented or enabled by default.

10. Epilogue

By disassembling the binary (with objdump), one precisely see what gcc creates during the compilation process. In this article, I explained the main options one can use to protect a program from a buffer overflow attack. There are several other options (-fstack-protector-explicit, -fstack-check, -fstack-clash-protection) to precisely manage the response to a stack smash attack.

Even if the default GCC option protects programs compiled with GCC from stack buffer overflows, there are still plenty of them found in the CVE database which shows that we can't rely on default protections to feel secure.

In the next articles of this series, I will continue to study the options GCC offers to protect a binary from attackers.

Top comments (0)