¿Cuál es la opción -fPIE para ejecutables independientes de la posición en gcc y ld?

Respuestas:

100

PIE es compatible con la aleatorización del diseño del espacio de direcciones (ASLR) en archivos ejecutables.

Antes de que se creara el modo PIE, el ejecutable del programa no se podía colocar en una dirección aleatoria en la memoria, solo las bibliotecas dinámicas de código independiente de la posición (PIC) podían reubicarse en un desplazamiento aleatorio. Funciona de manera muy similar a lo que hace PIC para las bibliotecas dinámicas, la diferencia es que no se crea una tabla de vinculación de procedimiento (PLT), sino que se usa la reubicación relativa a la PC.

Después de habilitar el soporte de PIE en gcc / linkers, el cuerpo del programa se compila y se vincula como código independiente de la posición. Un enlazador dinámico realiza un procesamiento de reubicación completo en el módulo del programa, al igual que las bibliotecas dinámicas. Cualquier uso de datos globales se convierte en acceso a través de la Tabla de compensaciones globales (GOT) y se agregan reubicaciones GOT.

PIE está bien descrito en esta presentación de OpenBSD PIE .

Los cambios en las funciones se muestran en esta diapositiva (PIE vs PIC).

foto x86 vs pastel

Las variables y funciones globales locales se optimizan en forma circular

Las variables y funciones globales externas son las mismas que en la imagen

y en esta diapositiva (PIE vs enlace antiguo)

Pie x86 vs sin banderas (arreglado)

Las variables y funciones globales locales son similares a las fijas.

Las variables y funciones globales externas son las mismas que en la imagen

Tenga en cuenta que PIE puede ser incompatible con -static

osgx
fuente
3
También en wikipedia: en.wikipedia.org/wiki/…
osgx
5
¿Por qué -pie y -static son compatibles en ARM y NO son compatibles en x86? Mi pregunta SO: stackoverflow.com/questions/27082959/…
4ntoine
56

Ejemplo de ejecución mínima: GDB el ejecutable dos veces

Para aquellos que quieran ver alguna acción, veamos cómo ASLR trabaja en el ejecutable PIE y cambia las direcciones entre ejecuciones:

C Principal

#include <stdio.h>

int main(void) {
    puts("hello");
}

main.sh

#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
  exe="${pie}.out"
  gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
  gdb -batch -nh \
    -ex 'set disable-randomization off' \
    -ex 'break main' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    "./$exe" \
  ;
  echo
  echo
done

Para el que tiene -no-pie, todo es aburrido:

Breakpoint 1 at 0x401126: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

Antes de comenzar la ejecución, break mainestablece un punto de interrupción en 0x401126.

Luego, durante ambas ejecuciones, se rundetiene en la dirección 0x401126.

El que tiene -piesin embargo es mucho más interesante:

Breakpoint 1 at 0x1139: file main.c, line 4.

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x5630df2d6139

Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x55763ab2e139

Antes de iniciar la ejecución, el BGF sólo se necesita una dirección "ficticia" que está presente en el archivo ejecutable: 0x1139.

Sin embargo, después de que se inicia, GDB advierte inteligentemente que el cargador dinámico colocó el programa en una ubicación diferente y la primera pausa se detuvo en 0x5630df2d6139.

Luego, la segunda ejecución también notó inteligentemente que el ejecutable se movió nuevamente y terminó rompiendo en 0x55763ab2e139.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_spacegarantiza que ASLR esté activado (el valor predeterminado en Ubuntu 17.10): ¿Cómo puedo deshabilitar temporalmente ASLR (distribución aleatoria del diseño del espacio de direcciones)? | Pregúntale a Ubuntu .

set disable-randomization offde lo contrario, GDB, como su nombre indica, desactiva ASLR para el proceso de forma predeterminada para proporcionar direcciones fijas entre ejecuciones para mejorar la experiencia de depuración: ¿ Diferencia entre direcciones gdb y direcciones "reales"? | Desbordamiento de pila .

readelf análisis

Además, también podemos observar que:

readelf -s ./no-pie.out | grep main

da la dirección de carga de tiempo de ejecución real (la PC señaló la siguiente instrucción 4 bytes después):

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

mientras:

readelf -s ./pie.out | grep main

da solo un desplazamiento:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

Al desactivar ASLR (con randomize_va_spaceo set disable-randomization off), GDB siempre da mainla dirección:, 0x5555555547a9por lo que deducimos que la -piedirección se compone de:

0x555555554000 + random offset + symbol offset (79a)

TODO, ¿dónde está codificado 0x555555554000 en el kernel de Linux / cargador glibc / donde sea? ¿Cómo se determina la dirección de la sección de texto de un ejecutable PIE en Linux?

Ejemplo de montaje mínimo

Otra cosa interesante que podemos hacer es jugar con algún código ensamblador para entender más concretamente lo que significa PIE.

Podemos hacer eso con un ensamblaje independiente de Linux x86_64 hello world:

red eléctrica

.text
.global _start
_start:
asm_main_after_prologue:
    /* write */
    mov $1, %rax   /* syscall number */
    mov $1, %rdi   /* stdout */
    mov $msg, %rsi  /* buffer */
    mov $len, %rdx /* len */
    syscall

    /* exit */
    mov $60, %rax   /* syscall number */
    mov $0, %rdi    /* exit status */
    syscall
msg:
    .ascii "hello\n"
len = . - msg

GitHub aguas arriba

y se ensambla y funciona bien con:

as -o main.o main.S
ld -o main.out main.o
./main.out

Sin embargo, si intentamos vincularlo como PIE con ( --no-dynamic-linkerse requiere como se explica en: ¿Cómo crear un ELF ejecutable independiente de posición vinculada estáticamente en Linux? ):

ld --no-dynamic-linker -pie -o main.out main.o

entonces el enlace fallará con:

ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output

Porque la línea:

mov $msg, %rsi  /* buffer */

codifica la dirección del mensaje en el movoperando y, por lo tanto, no es independiente de la posición.

Si, en cambio, lo escribimos de forma independiente de la posición:

lea msg(%rip), %rsi

entonces el enlace PIE funciona bien, y GDB nos muestra que el ejecutable se carga en una ubicación diferente en la memoria cada vez.

La diferencia aquí es que leacodificó la dirección msgrelativa a la dirección actual de la PC debido a la ripsintaxis, consulte también: ¿Cómo usar el direccionamiento relativo RIP en un programa ensamblador de 64 bits?

También podemos averiguarlo desmontando ambas versiones con:

objdump -S main.o

que dan respectivamente:

e:   48 c7 c6 00 00 00 00    mov    $0x0,%rsi
e:   48 8d 35 19 00 00 00    lea    0x19(%rip),%rsi        # 2e <msg>

000000000000002e <msg>:
  2e:   68 65 6c 6c 6f          pushq  $0x6f6c6c65

Entonces vemos claramente que leaya tiene la dirección correcta completa msgcodificada como dirección actual + 0x19.

Sin movembargo, la versión ha establecido la dirección en 00 00 00 00, lo que significa que se realizará una reubicación allí: ¿Qué hacen los enlazadores? El críptico R_X86_64_32Sen el ldmensaje de error es el tipo real de reubicación que se requirió y que no puede ocurrir en los ejecutables PIE.

Otra cosa divertida que podemos hacer es poner el msgen la sección de datos en lugar de .textcon:

.data
msg:
    .ascii "hello\n"
len = . - msg

Ahora el .oensamblaje para:

e:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 15 <_start+0x15>

por lo que el desplazamiento RIP es ahora 0, y suponemos que el ensamblador ha solicitado una reubicación. Lo confirmamos con:

readelf -r main.o

lo que da:

Relocation section '.rela.text' at offset 0x160 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  000200000002 R_X86_64_PC32     0000000000000000 .data - 4

tan claramente R_X86_64_PC32es una reubicación relativa de PC que ldpuede manejar los ejecutables PIE.

Este experimento nos enseñó que el propio enlazador comprueba que el programa puede ser PIE y lo marca como tal.

Luego, al compilar con GCC, -piele dice a GCC que genere un ensamblado independiente de la posición.

Pero si escribimos ensamblaje nosotros mismos, debemos asegurarnos manualmente de haber logrado la independencia de posición.

En ARMv8 aarch64, la posición hello world independiente se puede lograr con la instrucción ADR .

¿Cómo determinar si un ELF es independiente de la posición?

Además de ejecutarlo a través de GDB, se mencionan algunos métodos estáticos en:

Probado en Ubuntu 18.10.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
1
¡Hola Ciro! ¿Puede crear una pregunta separada para la dirección de inicio circular de ASLR-off y vincularla aquí?
osgx
1
@osgx Hecho. ¿Ya lo sabes o lo vas a desenterrar sobre la marcha? :-) Mientras lo hace, sería genial explicar cómo el cargador de kernel / dyn de Linux determina si algo tiene PIE o no: unix.stackexchange.com/questions/89211/…
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功
No lo sé ya, pero sé que debería extraerse de rtld de glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (si el intérprete sigue siendo ld-linux.so). Hace tres años, Basile no estaba seguro acerca de 0x55555555 y stackoverflow.com/questions/29856044 , pero esa pregunta era sobre la dirección de inicio de ld.so en sí, así que busque en los scripts fs / binfmt_elf.c o readelf / objdump y enlazador del kernel .
osgx