Position Independent Executables (PIEs)

Position independent code (PIC) is a term used to describe machine code that executes properly regardless of its absolute address when loaded into memory. Historically, code was position-dependent and the base address of the code needed to be loaded into the same location in memory in order to execute correctly. This is due to absolute addressing - some instructions were referencing absolute locations in memory for load and store instructions, etc. If position-dependent code were to be loaded into some different base address than what's expected by the programmer, the program would not execute correctly because instructions using absolute addressing would not be accessing appropriate values.

In contrast, position independent code uses relative addressing - references to locations in memory utilize some offset and the operands of instructions using relative addressing are calculated at runtime. The x86 and x86-64 architectures tackle the issue of resolving absolute addresses at runtime using two different methods:

  • x86 uses a fake function call in order to obtain a return address from the stack. This function usually looks like this in a debugger: __x86.get_pc_thunk.bx

The position of the code is loaded into the ebx register and instructions using relative addressing utilize this value in order to acquire the absolute address of their operand in memory. [1]

  • Instructions in x86-64 are able to acquire the absolute address of their operands by using the value of the RIP register with some offset. This is definitely more efficient than a fake function call.

Why do we need position independent code?

Position independent code enables our use of shared libraries. Shared library code can be loaded into any location within process memory and, because it's position independent, it will execute correctly. This also allows a process to load multiple shared libraries into memory without having to worry about address space collisions. [2] p. 1-3

How are symbols resolved for a shared library?

A shared library's exported symbols will be in different locations each time it is loaded into process memory. Fortunately for us, we have the Global Offset Table (GOT) mechanism to help us resolve the absolute address of the symbols at runtime. [3] When an object file is loaded into process memory, the dynamic linker processes the relocation entries of the file, determines the associated symbol values for the relocation entries, and then calculates the absolute addresses of the symbols. The dynamic linker then sets the GOT entries to the correct values for the symbols.

When an executable file makes a function call to a function exported by a shared library, the procedure linkage table will handle the function call and the dynamic linker will resolve the absolute address of the requested function. The dynamic linker then stores the absolute address of the function call in the executable's GOT, and execution is redirected using the absolute address that was resolved and stored in the executable's GOT. [4]

Exposition on the Procedure Linkage Table (PLT)

While the Global Offset Table handles position-independent address calculations to absolute locations, the PLT handles position-independent function calls to absolute locations. The linker is unable to resolve execution transfers (function calls) of position-independent code from one executable to another, or to a shared object, so it leverages this responsibility onto the program using the program's PLT.

The structure of the PLT is as follows:

  • The special first entry of the PLT - contains code to call the dynamic linker for symbol resolution, providing the linker with the offset into the symbol table and the address of a structure that identifies the location of the caller.
  • The succeeding structures in the PLT are used to handle function calls into shared objects.
    • The first address in each structure contains a JUMP instruction into the GOT entry for the symbol.
    • If the absolute address of the symbol is not resolved in the GOT, program execution returns back to this second entry in the symbol@PLT structure. This entry then PUSHes the ID of the symbol to be resolved to the stack. IDs for each symbol can be found in the _DYNAMIC section of the program.
    • This entry JUMPs into the first, special entry of the PLT - the call to the dynamic linker. The dynamic linker will use the ID previously PUSHed to the stack and attempt to resolve the desired symbol in the shared object. If the dynamic linker is able to resolve the absolute address of the symbol in the shared object, it stores the address in the GOT and then transfers execution to the function in the shared object. All future references to this symbol@PLT will immediately JUMP to the resolved address in the GOT.

The steps described above only occur if the program is leveraging "lazy linking", or Partial RELRO. When Full RELRO is enabled, all external symbols in the _DYNAMIC section are resolved to their absolute addresses in each shared object and marked r-x by the loader prior to program execution. [4]

So then, what's a position independent executable?

Position independent executables (PIEs) are executable files made entirely from position independent code. How this affects security and performance will be covered in the next section.

References

  1. https://stackoverflow.com/questions/6679846/what-is-i686-get-pc-thunk-bx-why-do-we-need-this-call
  2. ftp://bitsavers.informatik.uni-stuttgart.de/pdf/intel/iRMX/iRMX_86_Rev_6_Mar_1984/146196_Burst/iRMX_86_Application_Loader_Reference_Manual.pdf
  3. https://wiki.gentoo.org/wiki/Hardened/Position_Independent_Code_internals
  4. https://refspecs.linuxfoundation.org/ELF/zSeries/lzsabi0_zSeries/x2251.html