Что такое опция -fPIE для независимых от позиции исполняемых файлов в gcc и ld?

Ответы:

100

PIE предназначен для поддержки рандомизации разметки адресного пространства (ASLR) в исполняемых файлах.

До создания режима PIE исполняемый файл программы нельзя было разместить по случайному адресу в памяти, только динамические библиотеки позиционно-независимого кода (PIC) могли быть перемещены на случайное смещение. Он работает очень похоже на то, что PIC делает для динамических библиотек, разница в том, что таблица привязки процедур (PLT) не создается, вместо этого используется перемещение относительно ПК.

После включения поддержки PIE в gcc / компоновщиках тело программы компилируется и связывается как позиционно-независимый код. Динамический компоновщик выполняет полную обработку перемещения программного модуля, как и динамические библиотеки. Любое использование глобальных данных преобразуется в доступ через таблицу глобальных смещений (GOT), и добавляются перемещения GOT.

PIE хорошо описан в этой презентации OpenBSD PIE .

На этом слайде показаны изменения функций (PIE vs PIC).

x86 pic против пирога

Локальные глобальные переменные и функции оптимизированы в pie

Внешние глобальные переменные и функции такие же, как на рис.

и на этом слайде (PIE против старых ссылок)

x86 pie vs no-flags (исправлено)

Локальные глобальные переменные и функции аналогичны фиксированным

Внешние глобальные переменные и функции такие же, как на рис.

Обратите внимание, что PIE может быть несовместим с -static

osgx
источник
3
Также в википедии: en.wikipedia.org/wiki/…
osgx
5
Почему -pie и -static совместимы на ARM и НЕ совместимы на x86? Мой вопрос SO: stackoverflow.com/questions/27082959/…
4ntoine,
56

Минимальный исполняемый пример: GDB исполняемый файл дважды

Для тех, кто хочет увидеть какое-то действие, давайте посмотрим, как ASLR работает с исполняемым файлом PIE и меняет адреса между запусками:

main.c

#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

Тому, у кого -no-pieвсе скучно:

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

Перед запуском выполнения break mainустанавливает точку останова на 0x401126.

Затем во время обоих выполнений runостанавливается по адресу 0x401126.

Один с -pieоднако гораздо интереснее:

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

Перед началом выполнения, GDB просто принимает «фиктивный» адрес , который присутствует в исполняемый файл: 0x1139.

Однако после запуска GDB разумно замечает, что динамический загрузчик поместил программу в другое место, и первая остановка остановилась на этом 0x5630df2d6139.

Затем во втором прогоне также было разумно замечено, что исполняемый файл снова перемещается и в конечном итоге прерывается 0x55763ab2e139.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_spaceгарантирует, что ASLR включен (по умолчанию в Ubuntu 17.10): Как я могу временно отключить ASLR (рандомизация разметки адресного пространства)? | Спросите Ubuntu .

set disable-randomization offтребуется, иначе GDB, как следует из названия, по умолчанию отключает ASLR для процесса, чтобы давать фиксированные адреса между прогонами, чтобы улучшить процесс отладки: Разница между адресами gdb и «реальными» адресами? | Переполнение стека .

readelf анализ

Кроме того, мы также можем заметить, что:

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

дает фактический адрес загрузки во время выполнения (компьютер указал на следующую инструкцию через 4 байта):

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

пока:

readelf -s ./pie.out | grep main

дает просто смещение:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

При выключении ASLR (с помощью randomize_va_spaceили set disable-randomization off) GDB всегда выдает mainадрес:, 0x5555555547a9поэтому мы делаем вывод, что -pieадрес состоит из:

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

TODO где 0x555555554000 жестко закодирован в ядре Linux / загрузчике glibc / где угодно? Как в Linux определяется адрес текстовой части исполняемого файла PIE?

Пример минимальной сборки

Еще одна интересная вещь, которую мы можем сделать, - это поиграться с некоторым ассемблерным кодом, чтобы более конкретно понять, что означает PIE.

Мы можем сделать это с помощью автономной сборки Linux x86_64 hello world:

main.S

.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 вверх по течению

и он собирается и отлично работает с:

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

Однако, если мы попытаемся связать его как PIE с ( --no-dynamic-linkerтребуется, как описано в разделе: Как создать статически связанный независимый от позиции исполняемый файл ELF в Linux? ):

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

тогда ссылка не удастся:

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

Потому что строка:

mov $msg, %rsi  /* buffer */

жестко кодирует адрес сообщения в movоперанде и, следовательно, не зависит от позиции.

Если мы вместо этого напишем его независимым от позиции способом:

lea msg(%rip), %rsi

тогда ссылка PIE работает нормально, и GDB показывает нам, что исполняемый файл каждый раз загружается в другое место в памяти.

Разница здесь в том, что из-за синтаксиса leaкодируется адрес msgотносительно текущего адреса ПК rip, см. Также: Как использовать относительную адресацию RIP в 64-битной программе сборки?

Мы также можем выяснить это, разобрав обе версии с помощью:

objdump -S main.o

которые дают соответственно:

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

Итак, мы ясно видим, что leaуже есть полный правильный адрес, msgзакодированный как текущий адрес + 0x19.

movВерсия однако имеет установить адрес 00 00 00 00, который означает , что перемещение будет выполняться там: Что линкеры делать? Загадочный R_X86_64_32Sв ldсообщении об ошибке фактического типа перемещения , что необходимо , и что не может произойти в PIE исполняемых файлах.

Еще одна забавная вещь, которую мы можем сделать, - это поместить msgв раздел данных вместо .text:

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

Теперь .oсобирается:

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

так что смещение RIP сейчас 0, и мы предполагаем, что ассемблер запросил перемещение. Мы подтверждаем это:

readelf -r main.o

который дает:

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

так очевидно R_X86_64_PC32относительное перемещение ПК, которое ldможет обрабатывать исполняемые файлы PIE.

Этот эксперимент научил нас, что компоновщик сам проверяет, может ли программа быть PIE, и отмечает ее как таковую.

Затем при компиляции с помощью GCC -pieсообщает GCC о необходимости создания независимой от позиции сборки.

Но если мы сами пишем сборку, мы должны вручную убедиться, что достигли независимости позиции.

В ARMv8 aarch64 независимый от позиции привет мир может быть достигнут с помощью инструкции ADR .

Как определить, является ли ELF независимым от позиции?

Помимо запуска его через GDB, некоторые статические методы упоминаются по адресу:

Протестировано в Ubuntu 18.10.

Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1
Привет, Чиро! Можете ли вы создать отдельный вопрос для начального адреса ASLR-off pie-on и связать его здесь?
osgx
1
@osgx Готово. Вы это уже знаете или собираетесь откопать на лету? :-) Пока вы занимаетесь этим, было бы здорово объяснить, как загрузчик ядра / динамического процессора
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功
Я уже не знаю, но знаю, что его нужно копать из rtld glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (если интерпретатор все еще ld-linux.so). Три года назад Базиль не был уверен насчет 0x55555555 , а также stackoverflow.com/questions/29856044 , но этот вопрос касался начального адреса самого ld.so, поэтому копайтесь в скриптах ядра fs / binfmt_elf.c или readelf / objdump и компоновщика.
osgx