Що таке параметр -fPIE для незалежних від позицій виконуваних файлів у gcc та ld?


94

Як це змінить код, наприклад, виклики функцій?

Відповіді:


100

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


3
Також у wikipedia: en.wikipedia.org/wiki/…
osgx

5
Чому -pie та -static сумісні на ARM, а НЕ сумісні на x86? Мій С.О. питання: 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 / де завгодно? Як визначається адреса текстового розділу виконуваного файлу 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

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 .

Як визначити, чи ГНЧ не залежить від позиції?

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

Перевірено в Ubuntu 18.10.


1
Привіт Циро! Чи можете ви створити окреме запитання для початкової адреси відключення ASLR та зв’язати його тут?
osgx

1
@osgx Готово. Ви вже знаєте або збираєтесь викопати його на льоту? :-) Поки ви це робите, було б здорово пояснити, як ядро ​​Linux / завантажувач Dyn
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功

Я ще не знаю, але знаю, що його слід викопати з rtld glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (якщо перекладач все ще ld-linux.so). Три роки тому Basile був не впевнений , що 0x55555555 також stackoverflow.com/questions/29856044 , але питання було про стартовий адресу ld.so сам, тому копатися в ядрі ФС / binfmt_elf.c або readelf / objdump і компонувальник сценаріїв.
osgx
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.