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;
}
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
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>
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
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>
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@pltUnderstanding 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.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.
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.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
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
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
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 ?
- 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.
- 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)