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

DistributionRelease YearTypical glibc Version
Ubuntu 24.0420242.39
Ubuntu 22.0420222.35
Ubuntu 20.0420202.31
Ubuntu 18.0420182.27
Ubuntu 16.0420162.23
Ubuntu 14.0420142.19
Debian 9 Stretch20172.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:

OptionPurpose
--rmRemove the container after it exits.
-itRun interactively with a terminal.
-v "$PWD":/srcMount the current directory into the container.
-w /srcSet the working directory inside the container.
ubuntu:16.04Use 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:

FlagPurpose
-fno-stack-protectorDisables stack canaries. Useful for older stack-based exploits.
-z execstackMarks the stack as executable. Some shellcode-based exploits require this.
-no-pieDisables 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 found errors.
  • 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.