Linux binaries are often compiled with default settings that leave them vulnerable to common exploit techniques. Modern Linux distributions provide several compiler flags and linker options that can significantly harden these binaries, making them more resilient to attacks like buffer overflows and code injection. The primary tools for this are Position-Independent Executables (PIE), Read-Only Relocations (RELRO), and Stack Canaries.

Let’s see these in action. Imagine we have a simple C program:

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

void vulnerable_function(char* input) {
    char buffer[64];
    strcpy(buffer, input); // Vulnerable to buffer overflow
    printf("You entered: %s\n", buffer);
}

int main(int argc, char** argv) {
    if (argc != 2) {
        printf("Usage: %s <input>\n", argv[0]);
        return 1;
    }
    vulnerable_function(argv[1]);
    return 0;
}

If we compile this naively with gcc -o vulnerable vulnerable.c, an attacker could provide a very long string as input to argv[1], overwriting buffer and potentially executing arbitrary code.

PIE: Making Addresses Unpredictable

Position-Independent Executables (PIE) allow a program to be loaded into memory at any arbitrary address. This is crucial because it randomizes the base address of the executable in memory. Without PIE, an attacker might know the exact memory location of certain functions or data structures, allowing them to craft an exploit that jumps to a known address. With PIE, the attacker can no longer rely on fixed addresses.

Diagnosis: To check if a binary is PIE, use objdump:

objdump -p vulnerable | grep PIE

If you see PIE in the output, it’s PIE. If you see DYN (dynamic), it means it’s dynamically linked but not necessarily PIE. For a true PIE, you’d typically see Requesting program interpreter and interpreter fields. A more definitive check is readelf -h vulnerable | grep "Type:" which should show EXEC (executable) for non-PIE and DYN (Position-Independent Executable) for PIE.

Fix: Compile with the -fPIE and -pie flags:

gcc -fPIE -pie -o vulnerable_pie vulnerable.c

Why it works: The -fPIE flag tells the compiler to generate position-independent code (PIC) for the entire program, not just shared libraries. The -pie flag tells the linker to create a position-independent executable. When loaded by the operating system’s dynamic linker, the executable’s base address is randomized, making exploit addresses unreliable.

RELRO: Protecting Global Data

Read-Only Relocations (RELRO) is a linker feature that mitigates certain types of exploits by making the Global Offset Table (GOT) and Procedure Linkage Table (PLT) read-only after the program has been initialized. The GOT and PLT are crucial for dynamic linking, allowing the program to resolve the addresses of external functions (like strcpy or printf) at runtime. If an attacker can overwrite entries in the GOT, they can redirect function calls to malicious code.

There are two forms of RELRO:

  • Partial RELRO: Makes the GOT read-only after the dynamic linker has finished its work. This is the default on many modern systems.
  • Full RELRO: Makes both the GOT and the PLT read-only. This is more secure but can sometimes cause issues with certain dynamic linking scenarios.

Diagnosis: Use objdump to check RELRO status:

objdump -p vulnerable_pie | grep RELRO

You’ll see RELRO with either Partial or Full. If nothing is shown, it’s likely not enabled.

Fix: Compile with the -Wl,-z,relro flag for partial RELRO, and -Wl,-z,relro,-z,now for full RELRO. The -Wl, prefix passes options directly to the linker.

# For Partial RELRO (often default, but explicit is good)
gcc -fPIE -pie -Wl,-z,relro -o vulnerable_partial_relro vulnerable.c

# For Full RELRO
gcc -fPIE -pie -Wl,-z,relro,-z,now -o vulnerable_full_relro vulnerable.c

Why it works:

  • Partial RELRO (-z,relro): After the dynamic linker resolves all necessary addresses and populates the GOT/PLT, the section containing them is marked as read-only. This prevents an attacker from overwriting GOT entries later in the program’s execution.
  • Full RELRO (-z,now): This flag forces all dynamic symbol resolutions to happen at program startup, before the GOT/PLT are marked read-only. This means the GOT/PLT are effectively read-only from the moment they are used, providing stronger protection against GOT overwrite attacks.

Stack Canaries: Detecting Buffer Overflows

Stack canaries are small, random values placed on the stack just before a function’s return address. When a function is called, the canary is pushed onto the stack. Before the function returns, the canary is checked. If the canary’s value has changed, it means a buffer overflow has likely occurred, overwriting the canary along with other stack data. The program then aborts, preventing the attacker from hijacking the control flow.

Diagnosis: You can’t directly "see" a canary in a compiled binary’s output in a simple way. However, you can infer its presence by observing the program’s behavior when a buffer overflow would occur. If the program reliably crashes with a "stack smashing detected" error (or similar, depending on the libc implementation), canaries are likely enabled.

Fix: Compile with the -fstack-protector-all flag. Other options exist like -fstack-protector (which protects only functions with buffers that could overflow) or -fstack-protector-strong (a good balance), but -fstack-protector-all is the most comprehensive.

gcc -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-all -o vulnerable_hardened vulnerable.c

Why it works: The compiler inserts code at the beginning of functions to push a random canary value onto the stack. It also inserts code before function returns to verify that the canary value remains unchanged. If an overflow corrupts the canary, the verification fails, and the program terminates safely. The randomization of the canary value means an attacker cannot simply guess its value.

The Full Picture

By combining these flags, you create a much more secure executable:

gcc -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-all -o hardened_binary main.c

This command produces a binary that is:

  • PIE: Its base address in memory is randomized.
  • Full RELRO: The GOT/PLT are read-only after initialization, preventing GOT overwrite attacks.
  • Stack Canaries: Buffer overflows on the stack are detected, preventing control flow hijacking.

Even with these protections, vulnerabilities might still exist, but common exploit techniques become significantly harder to execute. For instance, a heap overflow or use-after-free vulnerability would not be caught by stack canaries.

The next hurdle you’ll face is ensuring your dynamically linked libraries are also hardened, or dealing with vulnerabilities that bypass these specific protections.

Want structured learning?

Take the full Cdk course →