Overview
Older public exploits often fail when compiled directly on a modern Kali system. This is especially common with older Linux kernel exploits, 32-bit targets, and proof-of-concept code written years ago against older compiler behaviour or older glibc versions.
The usual problem is not always the exploit code itself. The issue is often the build environment.
Modern Kali uses newer versions of gcc, glibc, linker defaults, hardening options, and compilation behaviour. A binary compiled on a modern system may expect newer runtime libraries than the target has available, or it may be built with protections that break older exploit assumptions.
Using Docker gives us a clean way to compile the exploit inside an older Linux environment without downgrading our attacking machine.
Why Older Exploits Break
When compiling C exploits, the final binary may depend on libraries from the system where it was built. If that binary is copied to an older target, it may fail because the target does not have the same glibc version.
A common error looks like this:
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.xx' not found
This means the binary was compiled against a newer glibc than the target supports.
Other common issues include:
- The exploit expects an executable stack.
- The exploit assumes stack protections are disabled.
- The exploit fails because the binary was compiled as PIE.
- The exploit must be compiled as 32-bit, but the required 32-bit libraries are missing.
- The target operating system is not Linux, for example FreeBSD.
The goal is to compile the exploit in an environment that is closer to the target.
Check the Target glibc Version
On the target, check the installed glibc version:
ldd --version
Example output:
ldd (Ubuntu GLIBC 2.23-0ubuntu11.3) 2.23
If the target uses glibc 2.23, compiling inside an Ubuntu 16.04 container is usually a better choice than compiling directly on modern Kali.
Quick glibc Reference
| Distribution | Release Year | Typical glibc Version |
|---|---|---|
| Ubuntu 24.04 | 2024 | 2.39 |
| Ubuntu 22.04 | 2022 | 2.35 |
| Ubuntu 20.04 | 2020 | 2.31 |
| Ubuntu 18.04 | 2018 | 2.27 |
| Ubuntu 16.04 | 2016 | 2.23 |
| Ubuntu 14.04 | 2014 | 2.19 |
| Debian 9 Stretch | 2017 | 2.24 |
This table should be treated as a quick lab reference, not a perfect rule for every system. The target may have been patched, upgraded, or built differently.
Basic Docker Compile Workflow
Place the exploit source code in your current working directory.
For example:
exploit.c
Then compile it inside an older Ubuntu container:
docker run --rm -it \
-v "$PWD":/src \
-w /src \
ubuntu:16.04 \
bash -lc 'apt-get update && apt-get install -y build-essential && gcc exploit.c -o exploit'
What this does:
| Option | Purpose |
|---|---|
--rm | Remove the container after it exits. |
-it | Run interactively with a terminal. |
-v "$PWD":/src | Mount the current directory into the container. |
-w /src | Set the working directory inside the container. |
ubuntu:16.04 | Use Ubuntu 16.04 as the build environment. |
bash -lc '...' | Run the compile commands inside the container. |
After the command finishes, the compiled binary appears in your current directory on Kali because the directory is mounted into the container.
Compile with Common Exploit Flags
Some older exploits require specific compiler and linker flags.
docker run --rm -it \
-v "$PWD":/src \
-w /src \
ubuntu:16.04 \
bash -lc 'apt-get update && apt-get install -y build-essential && gcc -fno-stack-protector -z execstack -no-pie exploit.c -o exploit'
The flags mean:
| Flag | Purpose |
|---|---|
-fno-stack-protector | Disables stack canaries. Useful for older stack-based exploits. |
-z execstack | Marks the stack as executable. Some shellcode-based exploits require this. |
-no-pie | Disables position independent executable output. Some older exploits expect fixed addresses. |
These flags should not be used blindly. They are useful when compiling exploit code in a lab, but they intentionally remove hardening features.
A compact one-liner version:
docker run --rm -it -v "$PWD":/src -w /src ubuntu:16.04 bash -lc 'apt-get update && apt-get install -y build-essential && gcc -fno-stack-protector -z execstack -no-pie exploit.c -o exploit'
Ready-to-Use Build Commands
Ubuntu 16.04 / glibc 2.23
docker run --rm -it -v "$PWD":/src -w /src ubuntu:16.04 \
bash -lc "apt-get update && apt-get install -y build-essential && gcc exploit.c -o exploit"
Ubuntu 18.04 / glibc 2.27
docker run --rm -it -v "$PWD":/src -w /src ubuntu:18.04 \
bash -lc "apt-get update && apt-get install -y build-essential && gcc exploit.c -o exploit"
Debian 9 Stretch / glibc 2.24
docker run --rm -it -v "$PWD":/src -w /src debian:stretch \
bash -lc "apt-get update && apt-get install -y build-essential && gcc exploit.c -o exploit"
Using Official GCC Docker Images
Sometimes the issue is not only the Linux distribution, but the compiler version itself. For very old exploit code, an older gcc version may compile more cleanly.
Pull a specific GCC image:
docker pull gcc:4.9
Compile the exploit:
docker run --rm \
-v "$PWD":/usr/src/myapp \
-w /usr/src/myapp \
gcc:4.9 \
gcc -o exploit exploit.c
Check the resulting binary:
file exploit
Example output:
exploit: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, not stripped
For an interactive shell inside the container:
docker run --rm -it \
-v "$PWD":/usr/src/myapp \
-w /usr/src/myapp \
gcc:4.9 \
bash
Then compile manually from inside the container:
gcc -o exploit exploit.c
Compiling 32-bit Binaries
If the target is 32-bit, compile the exploit as 32-bit with -m32.
Start an interactive Debian container:
docker run --rm -it -v "$PWD":/src -w /src debian:stable bash
Install the required dependencies:
apt-get update
apt-get install -y gcc-multilib
Compile a 32-bit binary:
gcc -m32 exploit.c -o exploit32
Check the result:
file exploit32
Expected output should include something like:
ELF 32-bit LSB executable
Static Compilation
Static compilation bundles required libraries into the binary. This can help avoid glibc mismatch errors when moving the binary to an older target.
For a 32-bit static binary:
gcc -m32 -static exploit.c -o exploit32-static
For a 64-bit static binary:
gcc -static exploit.c -o exploit64-static
Then check the binary:
file exploit64-static
ldd exploit64-static
For a fully static binary, ldd should usually return:
not a dynamic executable
Optional: Strip the Binary
To reduce the file size:
strip exploit64-static
This removes symbols from the binary. It can make uploads quicker, but avoid stripping if you still need to debug the binary.
Static vs Dynamic: Which Should You Use?
Use a dynamic binary when:
- You are compiling on a similar OS version to the target.
- The target has compatible libraries.
- You want a smaller binary.
Use a static binary when:
- You keep getting
GLIBC_x.xx not founderrors. - The target is old or minimal.
- You want to avoid relying on target libraries.
Static binaries are often larger, but they are more portable in lab environments.
FreeBSD Targets
Linux binaries will not run on FreeBSD targets. If the target is FreeBSD, compile directly on the FreeBSD system using its native toolchain.
Upload the source code to the target and compile it there:
cc exploit.c -o exploit
On FreeBSD, cc is usually clang.
If the exploit requires similar hardening-related flags, you can try:
cc -fno-stack-protector -z execstack exploit.c -o exploit
You usually do not need -m32 or -m64 unless you are doing something specific. First confirm the architecture:
uname -m
Then compile for that architecture.
Troubleshooting
GLIBC_x.xx not found
The binary was compiled against a newer glibc than the target supports.
Try compiling in an older container:
docker run --rm -it -v "$PWD":/src -w /src ubuntu:16.04 bash
Or compile statically:
gcc -static exploit.c -o exploit
fatal error: bits/libc-header-start.h: No such file or directory
This often happens when trying to compile 32-bit code without the required multilib packages.
Install multilib support:
apt-get update
apt-get install -y gcc-multilib
Then compile again:
gcc -m32 exploit.c -o exploit32
gcc: error: unrecognized command-line option '-no-pie'
Older GCC versions may not support -no-pie.
Try removing it:
gcc -fno-stack-protector -z execstack exploit.c -o exploit
Or use this linker syntax instead:
gcc -fno-stack-protector -z execstack -Wl,-no-pie exploit.c -o exploit
permission denied after uploading the binary
Make the binary executable on the target:
chmod +x exploit
Then run it:
./exploit
Wrong architecture
If the binary fails with an execution error, confirm the target architecture:
uname -m
Check your compiled binary:
file exploit
If the target is 32-bit but the binary is 64-bit, recompile with -m32.
Practical Lab Workflow
A simple workflow for older Linux exploits:
# 1. Check target architecture and glibc
uname -m
ldd --version
# 2. Place exploit source locally
ls -la exploit.c
# 3. Compile inside an older container
docker run --rm -it -v "$PWD":/src -w /src ubuntu:16.04 \
bash -lc 'apt-get update && apt-get install -y build-essential && gcc -fno-stack-protector -z execstack -no-pie exploit.c -o exploit'
# 4. Check the output
file exploit
# 5. Transfer to target and execute
chmod +x exploit
./exploit
For 32-bit targets:
docker run --rm -it -v "$PWD":/src -w /src debian:stable bash
apt-get update
apt-get install -y gcc-multilib
gcc -m32 -static exploit.c -o exploit32
file exploit32
Key Takeaways
When an older exploit fails, do not immediately assume the exploit is broken. First check whether the compile environment matches the target closely enough.
Docker is useful because it allows you to quickly switch between older build environments without modifying your Kali installation. For older Linux targets, matching the target glibc version or compiling statically usually solves the most common problems. For 32-bit targets, remember to use -m32 and install multilib support. For FreeBSD, compile on FreeBSD rather than trying to run a Linux binary.