If you’ve been reading my posts, I’ve had some trouble running binaries due to the differences between libc.

If you haven’t read my previous post or don’t know what pwninit is, have a look here.

When you run an ELF that was built on an older version of Linux shipped with a different version of libc, from your system’s you get an error.

Here’s an example using the binary from picoctf’s cache me outside challenge.

strings heapedit | grep Ubuntu
GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

I tried running the binary, but get an error.

./heapedit 
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!

By the way if you don’t download the libc.so.6, you won’t get an error when executing the file.

However if you download the ELF file as well as the libc.so.6 file you’ll encounter this problem.

So why does this error occur and how does pwninit fix it?

pwninit is a tool written in Rust, which is a wrapper around patchelf.

patchelf allows you to change the dynamic loader(ld) and the RPATH for your ELF binaries.

When you create an ELF file with the linker (ld), it stores a list of directories in the ELF’s .dynamic section.

The list is called RPATH or the RUNPATH.

When you run your ELF file, the system’s dynamic linker (ld.so) reads the RPATH.

It searches the RPATH before the standard system library locations like (/lib or /usr/lib).

RPATH can use special tokens like $ORIGIN, which expands to the absolute path of the directory where the ELF file itself is located.

However, for CTFs, pwninit simply sets the RPATH to . (a single dot), which means “the current working directory.

Let’s go back to when we ran ./heapedit .

Here’s what in does step by step when we run ./heapedit .

1. Kernel Loads Binary

When you first execute ./heapedit, the Linux Kernel loads the binary into memory.

2. Kernel Reads PT_INTERP

Then the kernel looks at the binary’s PT_INTERP to see which dynamic linker to use.

The PT_INTERP is a small piece of information that’s embedded in the ELF file.

It’s a specific entry in a list called the Program Header Table.

THE PT_INTERP describes the path to the dynamic linker, it usually points to a string section inside the ELF called .interp which contains the path.

In my case it points to /lib64/ld-linux-x86-64.so.2 (Ubuntu 24.04 WSL).

3. New Linker Takes over

The system’s linker (part of glibc 2.39) starts up, ready to load the binary’s dependencies.

The new linker reads the binary’s RPATH and sees it must search the current directory $ORIGIN first.

4. Finding libc

It scans the directory and finds the libc.so.6.

However this isn’t glibc 2.39, it’s glibc 2.27 the old one from the challenge.

5. Crash

My linker (2.39) attempts to load and process the old libc (2.27) .

The internal data structures, symbol tables, and relocation formats are different. The new linker doesn’t know how to read this old file, and it segfaults.

The program dies before a single line of its own code ever runs.

How does pwninit fix this issue?

1. Downloads old linker

pwninit downloads the old linker ld-2.27.so that it matches the libc.so.6.

2. Patch binary’s PT_INTERP

pwninit uses patchelf to patch the PT_INTERP of the binary to point to the downloaded ld-2.27.so instead of the systems’s ld-so.

The moral of the story is that the dynamic-linker(ld) and the libc are a set and that their versions always need to match.

We conventionally have been calling it the “linker”, but in my opinion it feels like it’s a bit bit ambiguous to use “linker”, I think we should better names like the program loader or dynamic linker.

The program loader or dynamic linker has a file format of ld-linux.so.2 or ld-2.29.so.

Then why does it have to be same version as libc?

The dynamic linker and the libc.so.6 aren’t different projects.

They are each halves of the same glibc project and they were designed to work only with each other.

In short running pwninit solves these problems because, it set the dynamic-linker version to match the libc version.

pwninit
bin: ./heapedit
libc: ./libc.so.6

fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.2_amd64.deb
setting ./ld-2.27.so executable
copying ./heapedit to ./heapedit_patched
running patchelf on ./heapedit_patched

Now we can understand why ld.so has teh glibc version in it’s file name.

./heapedit_patched 
You may edit one byte in the program.
Address: 0xabcd
Value: t help you: this is a random string.

Here’s the ldd result before running pwninit and after.

ldd heapedit
linux-vdso.so.1 (0x00007ffd7ffaf000)
libc.so.6 => ./libc.so.6 (0x00007f8e00400000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8e00914000)
ldd heapedit_patched 
linux-vdso.so.1 (0x00007fff565cd000)
libc.so.6 => ./libc.so.6 (0x00007f96ade00000)
./ld-2.27.so => /lib64/ld-linux-x86-64.so.2 (0x00007f96ae2ff000)

Voila, you can see that the system is using the old ld-2.27.so instead of the system’s ld.

So to answer the question should you run pwninit, even if you can run the binary?

The answer is always yes, if the binary used a different version of libc from your system’s.

Here are some extra reading materials if you want to get a deeper understanding.

Extra Reading Materials

  1. https://www.lurklurk.org/linkers/linkers.html

  2. https://www.cs.dartmouth.edu/sergey/cs108/ABI/UlrichDrepper-How-To-Write-Shared-Libraries.pdf