PIE (Position Independent Execution)

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 06:23, 22 января 2018.
Open book.svg Авторство
В. М. Виноградова
Согласовано: 22 января 2018 года

PIE (англ. Position Independent Execution – Независимый от расположения исполняемый файл) – исполняемые файлы, полностью созданные из кода, не зависящего от местоположения.

Позиционно-независимый код (PIC)

Независимый от местоположения код (Position-Independent Code) - это код, который может быть размещена в любой области памяти, поскольку все ссылки на ячейки памяти в нем относительные (например, относительно счётчика команд). Возможность создания позиционно-независимого кода зависит от архитектуры и системы команд целевой платформы. К примеру, если во всех инструкциях перехода в системе команд должны указываться абсолютные адреса, то код, требующий переходов, практически нельзя сделать позиционно-независимым. В архитектуре x86 непосредственная адресация в инструкциях работы с данными представлена только абсолютными адресами, но поскольку адреса данных считаются относительно сегментного регистра, который можно поменять в любой момент, это даёт возможность создавать позиционно-независимый код со своими ячейками памяти для данных. Кроме того, некоторые ограничения набора команд могут сниматься с помощью самомодифицирующегося кода или нетривиальных последовательностей инструкций.

PIС обычно используется для разделяемых библиотек, так что один и тот же библиотечный код может быть загружен в местоположение в каждом адресном пространстве программы, где он не будет перекрывать другие виды использования памяти (например, другие разделяемые библиотеки). PIC также использовался на старых компьютерных системах, не имеющих MMU[Источник 1], так что операционная система могла удержать приложения друг от друга даже в пределах одного адресного пространства системы с MMU-less.

Независимый от позиции код может быть выполнен на любом адресе памяти без модификации. Это отличается от перемещаемого кода, в котором редактор ссылок или программный загрузчик модифицирует программу перед выполнением, чтобы ее можно было запускать только из определенного места в памяти. Генерация независимого от позиции кода часто является стоит по умолчанию для компиляторов, но они могут накладывать ограничения на использование некоторых функций языка, например: запрет использования абсолютных адресов (код, зависящий от положения, должен использовать относительную адресацию). Инструкции, которые ссылаются непосредственно на конкретные адреса памяти, иногда выполняются быстрее, и замена их эквивалентными инструкциями относительной адресации может привести к немного более медленному выполнению, хотя современные процессоры делают разницу практически незначительной.

История

На первых компьютерах, таких как IBM System/360 (1965), код был зависимым от местоположения: каждая программа была создана для загрузки и запуска с определенного адреса. Если многозадачная операционная система допускала одновременное выполнение нескольких заданий с использованием отдельных программ, операции должны были быть запланированы таким образом, чтобы никакие два параллельных задания не запускали программы, требующие одинаковые адреса загрузки. Например, как программа расчета заработной платы, так и программа учета дебиторской задолженности, созданная для работы по адресу 32K, не могут одновременно выполняться одновременно. IBM DOS / 360 (1966) не имел возможности перемещать программы во время загрузки. Иногда поддерживалось несколько версий программы, каждая из которых была построена для другого загружаемого адреса. Специальный класс программ, называемых самопересекающимися программами, был закодирован, чтобы передислоцироваться после загрузки. IBM OS / 360 (1966) переносила исполняемые программы, когда они были загружены в память. Требовалась только одна копия программы, однако после загрузки программа не может быть перемещена.

Для сравнения, на ранних сегментированных или базовых и граничных системах, таких как Burroughs B5000 (1961) и Multics (1964), код был по своей природе не зависящим от местоположения, поскольку адреса в программе относились к текущему сегменту, а не к абсолютному. Независимый от позиции код был разработан для устранения этих ограничений для несегментированных систем. PIEа может быть загружена по любому адресу в памяти. Изобретение динамического преобразования адреса (функция, предоставляемая MMU) изначально уменьшило потребность в PIC, потому что каждый процесс мог иметь свое собственное независимое адресное пространство (диапазон адресов). Тем не менее, несколько одновременных заданий с использованием одного и того же кода создали пустую трату виртуальной памяти. Если два задания выполняются полностью идентичными программами, динамическое преобразование адресов обеспечивает решение, позволяя системе просто сопоставлять адрес 32K двум разным заданиям с теми же байтами реальной памяти, содержащими единственную копию программы.

Различные программы могут использовать общий код. Например, программа расчета заработной платы и программа дебиторской задолженности могут содержать как идентичную подпрограмму сортировки. Общий модуль (разделяемая библиотека представляет собой форму совместно используемого модуля) загружается один раз и отображается в два адресных пространства.[Источник 2]

Технические детали

Вызовы процедур внутри разделяемой библиотеки, как правило, выполняются с помощью небольших частей таблицы связывания процедур, которые затем вызывают окончательную функцию. Это позволяет библиотеке наследовать некоторые вызовы функций из ранее загруженных библиотек, а не использовать собственные версии. Ссылки на данные из позиционно-независимого кода обычно делаются косвенно через глобальные таблицы смещения (GOT), которые хранят адреса всех доступных глобальных переменных. Существует один GOT на единицу компиляции или объектный модуль, и он расположен с фиксированным смещением от кода (хотя это смещение неизвестно, пока библиотека не связана). Когда компоновщик связывает модули для создания общей библиотеки, он объединяет GOT и устанавливает окончательные смещения в коде. Нет необходимости корректировать смещения при загрузке разделяемой библиотеки позже.

Позиционные независимые функции получают доступ к глобальным данным, путем определения абсолютного адреса GOT с учетом их собственного значения счетчика текущей программы. Это часто делается в форме поддельного вызова функции для получения возвращаемого значения в стеке (x86) или в специальном регистре (PowerPC, SPARC, [[MIPS (Microprocessor without Interlocked Pipeline Stages)|MIPS], возможно, по крайней мере, некоторые другие RISC-процессоры , ESA / 390) , которые затем могут быть сохранены в предопределенном стандартном регистре. Некоторые архитектуры процессоров, такие как Motorola 68000, Motorola 6809, WDC 65C816, Knuth's MMIX, ARM и x86-64, позволяют ссылаться на данные по смещению от счетчика программ. Это специально сделано для того, чтобы PIC стал меньше, менее требователен к регистру и, следовательно, более эффективным.[Источник 2]

Windows DLLs

Библиотеки динамической компоновки (DLL) в Microsoft Windows используют вариант E8 инструкции CALL. Эти инструкции не нужно исправлять, когда загружается DLL.

Предполагается, что некоторые глобальные переменные (например, массивы строковых литералов, таблицы виртуальных функций) содержат адрес объекта в разделе данных resp. в разделе кода динамической библиотеки, поэтому хранимый адрес в глобальной переменной необходимо обновить, чтобы отразить адрес, куда была загружена DLL. Динамический загрузчик вычисляет адрес, на который ссылается глобальная переменная, и сохраняет в нее значение, это вызывает копирование при записи той страницы памяти, что содержит эту ​​глобальную переменную. Страницы с кодом и страницы с глобальными переменными, которые не содержат указателей на код или глобальные данные, остаются общими между процессами. Эта операция выполянется в любой ОС, которая может загружать динамическую библиотеку по произвольному адресу.

В Windows Vista и более поздних версиях Windows перемещение DLL и исполняемых файлов выполняется менеджером памяти ядра, который совместно использует перемещенные двоичные файлы в нескольких процессах. Изображения всегда переносятся с предпочтительных базовых адресов, [[ASLR (Address Space Layout Randomization)|рандомизация размещения адресного пространства].

Версии Windows до Windows Vista требуют, чтобы DLL-файлы системы предварительно связывались по неконфликтующим фиксированным адресам во время соединения, чтобы избежать перемещения изображений во время выполнения. Перенос времени выполнения в этих старых версиях Windows выполняется загрузчиком DLL в контексте каждого процесса, и результирующие перемещаемые части каждого изображения больше не могут быть распределены между процессами.

Передача библиотек DLL в Windows отличается от более ранней процедуры OS / 2, из которой она извлекается. OS / 2 представляет третий вариант и пытается загрузить DLL, которые не являются позиционно-независимыми, в выделенную «общую арену» в памяти и отображает их после загрузки. Все пользователи DLL могут использовать одну и ту же копию в памяти.

Позиционно-независимый код в современных Unix-системах

Компиляторы современных систем семейства UNIX — GNU С или стандартный С-компилятор UNIX SVR4 имеют ключ -f PIC. Впрочем, код, порождаемый при использовании этого ключа, не является позиционно-независимым в указанном выше смысле: этот код все-таки содержит перемещаемые адресные ссылки. Задача состоит не в том, чтобы избавиться от таких ссылок полностью, а лишь в том, чтобы собрать все эти ссылки в одном месте и разместить их, по возможности, отдельно от кода. Это очень полезно при создании разделяемых библиотек.

Код, генерируемый GNU С, использует базовую адресацию: в начале функции адрес точки ее входа помещается в один из регистров, и далее вся адресация других функций и данных осуществляется относительно этого регистра. [Источник 3]На процессоре х86 используется регистр %ebx, а загрузка адреса осуществляется командами, вставляемыми в пролог каждой функции (пример 1).

На процессорах, где разрешен прямой доступ к счетчику команд, соответствующий код выглядит проще, но принцип сохраняется: компилятор занимает один регистр и благодаря этому упрощает работу загрузчику.

Пример 1. Получение адреса точки входа в позиционно-независимую подпрограмму

call L4
L4:
popl %ebx
.[Источник 3]

Как мы видим в примере 2, на самом деле адресация происходит не относительно точки входа в функцию, а относительно некоторого объекта, называемого GOT. Счетчик команд используется для вычисления адреса этой таблицы, а не сам по себе. Компилированный таким образом код предназначен в первую очередь для разделяемых библиотек формата ELF (формат исполняемых и собираемых [модулей], используемый большинством современных систем семейства Unix).

Пример 2. Позиционно-независимый код, порождаемый компилятором GNU С

/* strerror.c (emx+gcc)  Copyright (с) 1990-1996 by Eberhard Mattes */
#include <stdlib.h>
#include <string.h> 
#include <emx/thread.h>

char *strerror (int errnum) 
{ (
if (errnum >= 0 && errnum < _sys_nerr)
return (char *)_sys_errlist [errnum];
else
{ 
static char msg[] = "Unknown error "; 
#if defined ( _ MT _ )
struct _thread *tp = _thread ( ) ; 
#define result (tp->_th_error)
#else
static char result [32]; 
#endif
memcpy (result, msg, sizeof (rasg)  1) ;
_itoa (errnum, result + sizeof (msg)  1, 10) ;
return result;
}
}

gcc -f PIC -S strerror.c
. file "strerror" 
gcc2_compiled. :
_ gnu_compiled_c :
.data 
_msg.2:
.ascii "Unknown error \0" 
.Icomm _result.3,32 
.text
.align 2, 0x90
. globl _strerror
__strerror:
pushl %ebp
movl %esp, %ebp
pushl %ebx
call L4
L4:
popl %ebx
addl $_GLOBAL_OFFSET_TABLE_+ [ . -L4 ] , %ebx
cmpl $0,8 (%ebp)
jl L2
movl _ sys_nerr@GOT (%ebx) , %eax
movl 8 (%ebp) , %edx
cmpl %edx, (%eax)
jle L2
movl 8(%ebp),%eax
movl %eax,%edx
leal 0(,%edx,4),%eax
movl __sys_errlist@GOT(%ebx) , %edx
movl (%edx,%eax),%eax jmp LI
.align 2,0x90 jmp L3 .align 2,0x90
L2:
pushl $14
leal _msg.2@GOTOFF(%ebx),%edx
movl %edx,%eax
pushl %eax
leal _result.3@GOTOFF(%ebx) ,%edx
movl %edx,%eax
pushl %eax
call _memcpy@PLT
addl $12,%esp
pushl $10
leal _result.3@GOTOFF(%ebx) , %edx
leal 14(%edx),%eax
pushl %eax
movl 8(%ebp),%eax
pushl %eax
call __itoa@PLT
addl $12,%esp
leal _result.3@GOTOFF(%ebx) ,%edx
movl %edx,%eax
jmp LI
.align 2,0x90 
L3: 
LI:
movl -4 (%ebp),%ebx
leave
ret
.[Источник 3]

Влияние компиляции с PIE на результирующий двоичный файл

The Fedora Engineering Steering Committee поддерживает консервативный список пакетов, которые должны быть созданы с использованием функций безопасности GCC. Пакеты, не входящие в этот список, имеют функции безопасности, включенные по усмотрению упаковщиков. В настоящее время не существует консенсуса в отношении того, когда необходимы бинарные файлы с повышенной безопасностью. В результате, использование защищенных двоичных файлов может стать спорной темой. Большинство аргументов можно свести к тому, превышает ли выигрыш в безопасности производительность, связанную с использованием этой функции.[Источник 4]

Позиция Независимые исполняемые файлы (PIE) - это результат процесса упрочненного компоновки пакетов. Бинарный файл PIE и все его зависимости загружаются в случайные расположения в виртуальной памяти каждый раз, когда приложение выполняется. Это сильно затрудняет атаки Return-Oriented Programming (ROP).

Пример 3. Рассмотрим следующую программу «Hello World»:

#include "not/stdio.h"
char message[] = "Hello World";
int main(int argc, char *argv[], char *envp[])
{
    puts(message);
    return 0;
}
[Источник 4]

Для уменьшения постороннего влияния, в данном примере используется нестандартная реализация стандартных функций библиотеки во время компиляции:

$ cc -nostdlib -nodefaultlibs -I.   
                -o static-example   
                os/syscall.x86_64.s 
                os/start.x86_64.s   
                not/strlen.c        
                not/puts.c          
                main.c              

$ size --format=sysv static-example
static-example  :
section     size      addr
.text        420   4194536 
.rodata        2   4194956
.eh_frame    280   4194960
.data         12   6292392
.comment      44         0
Total        758
[Источник 4]

Двоичный ELF, который создается этой сборкой, не имеет зависимостей от libc или загрузчика для запуска. Это означает, что он может загружаться в память и запускаться независимо от компоновщика для динамического поиска и привязки с зависимостями. Однако это затрудняет совместное использование и повторное использование подпрограмм. Общим решением этой проблемы является создание общей библиотеки:

$ cc -fpic -shared -I. -nostdlib  
              -nodefaultlibs      
              -o libnotc.so       
              os/syscall.x86_64.s 
              os/syscall.c        
              not/strlen.c        
              not/puts.c
[Источник 4]

Следующий шаг - перекомпилировать основной двоичный код, указывающий, что некоторые определения символов существуют во внешней общей библиотеке:

$ cc -nostdlib -nodefaultlibs -I. 
              -o dynamic-example  
              os/start.x86_64.s   
              main.c -L. -lnotc
[Источник 4]

Размер результирующего двоичного файла имеет меньшую секцию .text, поскольку этот код содержится в общей библиотеке libnotc.so. Есть и другие существенные отличия:

$ size --format=sysv dynamic-example
dynamic-example  :
section              size      addr
.interp                28   4194816
.note.gnu.build-id     36   4194844
.gnu.hash              48   4194880
.dynsym               144   4194928
.dynstr                46   4195072
.rela.plt              48   4195120
.plt                   48   4195168
.text                  56   4195216
.eh_frame_hdr          28   4195272
.eh_frame              96   4195304
.dynamic              272   6292552
.got.plt               40   6292824
.data                  12   6292864
.comment               44         0
Total                 946
[Источник 4]

Чтобы программа корректно выполнялась, двоичный код ELF должен быть сконструирован таким образом, чтобы он позволял загрузчику разрешать символы во время выполнения. Поскольку адрес символа в памяти не является частью основного двоичного файла, загрузчик добавляет уровень косвенности в таблицу связей процедур (раздел .plt). Вместо прямого вызова puts () секция .plt содержит специальную запись, указывающую загрузчику. Затем загрузчик должен найти фактический адрес функции. После этого он обновляет запись в глобальной таблице сдвига (GOT). Последующие вызовы одной и той же процедуры выполняются переходами из записи GOT.

Стандартный двоичный файл ELF обычно загружается в тот же базовый адрес в виртуальной памяти каждый раз, когда он выполняется. Компоновщик использует это в неперемещаемом коде, перескакивая на абсолютные адреса символов. Это, как оказалось, имеет небольшое преимущество в производительности, поскольку быстрее перейти к абсолютному адресу, чем к относительной адресации. Это особенно верно для приложений i386, поскольку для этого требуется другой регистр.

Чтобы увидеть разницу между динамическими и PIE-приложениями, нам нужно перекомпилировать пример программы как PIE. Это просто требует добавления -fpic -pie-флагов к тому, что мы имели ранее:

$ cc -fpic -pie -nostdlib -nodefaultlibs -I. 
              -o pie-example  
              os/start.x86_64.s   
              main.c -L. -lnotc
$ size --format=sysv pie-example 
pie-example  :
section              size      addr
.interp                28       512
.note.gnu.build-id     36       540
.gnu.hash              52       576
.dynsym               192       632
.dynstr                54       824
.rela.dyn              24       880
.rela.plt              48       904
.plt                   48       960
.text                  61      1008
.eh_frame_hdr          28      1072
.eh_frame              96      1104
.dynamic              320   2098352
.got                    8   2098672
.got.plt               40   2098680
.data                  12   2098720
.comment               44         0
Total                1091
[Источник 4]

Обратите внимание, что адрес, указанный командой размера для каждой из секций ELF, является относительным адресом, тогда как адрес, указанный для динамического примера, использует абсолютное местоположение. Это необходимо, потому что программа и все ее зависимости будут загружены в случайные места в виртуальной памяти при выполнении. Это включает в себя предварительно связанные библиотеки и как таковая служит эффективной технологией предотвращения эксплойтов для атак, которые опираются на возврат к известным адресам стандартных системных библиотек.

Где используется PIE

Бинарные файлы PIE используются в некоторых дистрибутивах Linux, ориентированных на безопасность, чтобы позволить PaX или Exec Shield использовать [[ASLR (Address Space Layout Randomization)|рандомизация размещения адресного пространства], чтобы злоумышленники не знали, где существующий исполняемый код находится во время атаки безопасности, используя эксплойты, которые полагаются на знание смещения исполняемого кода в двоичных файлах, например обратные атаки на libc. Mac OS X и iOS Apple полностью поддерживают исполняемые файлы PIE, начиная с версий 10.7 и 4.3, соответственно; Появляется предупреждение, когда исполняемые файлы не-PIE iOS отправляются на утверждение в App Store Apple, но нет жесткого требования, так что приложения, не относящиеся к PIE, не обязательно будут отвергнуты. В OpenBSD PIE включен по умолчанию на большинстве архитектур начиная с OpenBSD 5.3, выпущенного 1 мая 2013 года. Поддержка PIE в статически связанных двоичных файлах, таких как исполняемые файлы в каталогах / bin и / sbin, была добавлена ​​ближе к концу 2014 года. Начиная с Fedora 23, разработчики Fedora решили создать пакеты с включенным PIE по умолчанию. Android осуществляет поддержку PIE в Jelly Bean и исключил линкерную поддержку без PIE в Lollipop.

Источники

  1. Левайн Джон Р. Linkers and Loaders // Chapter 8: Loading and overlays. — San Francisco: Morgan-Kauffman, 1999. — С. 170—171. — 1-55860-496-0.
  2. 2,0 2,1 Wikipedia/ [Электронный ресурс]: Position-independent code / Дата обращения: 15.05.2017. Режим доступа: https://en.wikipedia.org/wiki/Position-independent_code
  3. 3,0 3,1 3,2 Spectrum/ [Электронный ресурс]: Позиционно-независимый код / Дата обращения: 15.05.2017. Режим доступа: http://spectrumzx.ru/book/Glava%203/Index6.htm
  4. 4,0 4,1 4,2 4,3 4,4 4,5 4,6 Red Hat[Электронный ресурс]:Position Independent Executables (PIE)/ Дата обращения: 15.05.2017. Режим доступа: https://access.redhat.com/blogs/766093/posts/1975793.