Как это изменит код, например, вызовы функций?
Как это изменит код, например, вызовы функций?
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
Минимальный исполняемый пример: 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
анализКроме того, мы также можем заметить, что:
дает фактический адрес загрузки во время выполнения (компьютер указал на следующую инструкцию через 4 байта):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 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 вверх по течению
и он собирается и отлично работает с:
Однако, если мы попытаемся связать его как PIE с (
--no-dynamic-linker
требуется, как описано в разделе: Как создать статически связанный независимый от позиции исполняемый файл ELF в Linux? ):тогда ссылка не удастся:
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-битной программе сборки?Мы также можем выяснить это, разобрав обе версии с помощью:
которые дают соответственно:
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
, и мы предполагаем, что ассемблер запросил перемещение. Мы подтверждаем это:который дает:
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.
источник