We know we can check whether a binary has PIE enabled using the file command.

But how does the file command detect if the ELF file has PIE enabled or not.

Here’s the result on a 32 bit ELF without PIE.

file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=24c4fa8500082ef048a420baadc6a3d777d39f34, not stripped

This is the readelf output.

readelf -h vuln
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048520
  Start of program headers:          52 (bytes into file)
  Start of section headers:          6432 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         29
  Section header string table index: 28

Here’s the result of file on a 64bit ELF with PIE.

file vuln2 
vuln2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2942b90e61eee081d343dbe6a91c6ca85cbccd49, for GNU/Linux 3.2.0, not stripped

We can also see my machine has 2 dynamic loaders /lib64/ld-linux-x86-64.so.2 and /lib/ld-linux.so.2.

Since most computers these days are 64-bits, when you run apt-get install i386 I think it installs the dynamic loader for the 32-bit files.

This is the readelf output.

readelf -h vuln2
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1160
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14568 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

Let’s look at a shared object (libc.so) file this time.

readelf -h libc6-i386_2.27-3ubuntu1.5_amd64.so 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x19120
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1924108 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         10
  Size of section headers:           40 (bytes)
  Number of section headers:         68
  Section header string table index: 67

There’s a difference between binaries with PIE and ones without PIE.

The readelf -h command shows a big difference.

Type:                              EXEC (Executable file)
Type:                              DYN (Position-Independent Executable file)
Type:                              DYN (Shared object file)

The pie enabled binary has DYN for the type and the non pie binary has EXEC.

The linux kernel only accepts two types of ELF files ET_EXECand ET_DYN.

According to https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html

ET_DYN is a shared object file, but vuln2 isn’t, it’s just a PIE enabled binary.

Although it might seem like a mistake, this is actually correct.

PIE enabled binaries are intentionally marked as ET_DYN to enable ASLR.

ET_EXEC is the traditional executable where the linker creates a binary that is loaded at a fixed, predetermined virtual address.

The address is fixed which makes address randomization from ASLR hard.

ET_DYN allows kernel’s loader so it can map into a process’s address space at any available address.

For ASLR to be effective, the main executable itself must also be loaded at a random address.

What exactly is ASLR?

ASLR is a security feature built in to OS(Linux kernel).

It randomizes the starting address of key parts os a process’s memory every time it runs.

ASLR changes the base address for the stack(local variables and function calls), the heap(malloc, etc) and shared libraries like .so files including libc.so.

If the executable(which contains the main function) was always at the same location, attackers could still reliably use its code for ROP.

To solve this, GCC builds executables as PIE.

A PIE binary is essentially a shared object(ET_DYN) that is built to be executable.

If PIE enabled binaries and shared libraries are both ET_DYN, how does the kernel know which one is the executable and which one is a library?

A PIE executable will have a PT_INTERP segment in it’s header.

The PT_INTERP segment contains a string that specifies the path to the dynamic linker(/lib/ld-linux.so.2,/lib64/ld-linux-x86-64.so.2).

When the kernel sees an ET_DYN with a PT_INTERP segment it know it’s an executable.

A shared libary doesn’t have a PT_INTERP segment, therefore won’t be run directly and will thus be loaded by the dynamic linker.

Another fact we need to know is that, libc is always randomized by ASLR whether or not it’s a PIE binary because it’s a shared library.

The kernel handles the ASLR for libc.

https://stackoverflow.com/questions/34519521/why-does-gcc-create-a-shared-object-instead-of-an-executable-binary-according-to/34522357#34522357