Як це змінить код, наприклад, виклики функцій?
Відповіді:
PIE підтримує рандомізацію макета адресного простору (ASLR) у виконуваних файлах.
До того, як був створений режим PIE, виконуваний файл програми не міг бути розміщений за випадковою адресою в пам'яті, лише динамічні бібліотеки незалежного коду (PIC) могли бути переміщені у випадкове зміщення. Це дуже схоже на те, що робить PIC для динамічних бібліотек, різниця полягає в тому, що таблиця зв’язків процедур (PLT) не створюється, замість цього використовується відносне перенесення ПК.
Після увімкнення підтримки PIE у gcc / linkers, тіло програми компілюється та зв’язується як незалежний від позиції код. Динамічний компонувальник виконує повну обробку переміщення в програмному модулі, як і динамічні бібліотеки. Будь-яке використання глобальних даних перетворюється на доступ через таблицю глобальних компенсацій (GOT) і додаються переміщення GOT.
PIE добре описаний у цій презентації OpenBSD PIE .
Зміни функцій показані на цьому слайді (PIE проти PIC).
x86 pic vs 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
аналіз
Крім того, ми також можемо спостерігати, що:
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 / де завгодно? Як визначається адреса текстового розділу виконуваного файлу PIE у Linux?
Приклад мінімального складання
Ще одна крута річ, яку ми можемо зробити, це пограти з деяким кодом збірки, щоб більш конкретно зрозуміти, що означає PIE.
Ми можемо це зробити за допомогою окремої збірки Linux x86_64 hello world:
головний
.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
і він добре збирається і працює з:
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 .
Як визначити, чи ГНЧ не залежить від позиції?
Окрім простого запуску через GDB, деякі статичні методи згадуються за адресою:
Перевірено в Ubuntu 18.10.