¿Cómo cambiará el código, por ejemplo, llamadas a funciones?
Respuestas:
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
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 main
establece un punto de interrupción en 0x401126
.
Luego, durante ambas ejecuciones, se run
detiene en la dirección 0x401126
.
El que tiene -pie
sin 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_space
garantiza 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 off
de 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_space
o set disable-randomization off
), GDB siempre da main
la dirección:, 0x5555555547a9
por lo que deducimos que la -pie
direcció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
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-linker
se 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 mov
operando 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 lea
codificó la dirección msg
relativa a la dirección actual de la PC debido a la rip
sintaxis, 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 lea
ya tiene la dirección correcta completa msg
codificada como dirección actual + 0x19.
Sin mov
embargo, 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_32S
en el ld
mensaje 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 msg
en la sección de datos en lugar de .text
con:
.data
msg:
.ascii "hello\n"
len = . - msg
Ahora el .o
ensamblaje 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_PC32
es una reubicación relativa de PC que ld
puede 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, -pie
le 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.