почему код без pic не может быть полностью ASLR с помощью исправлений во время выполнения?

Я понимаю, что код PIC делает рандомизацию ASLR более эффективной и простой, поскольку код можно разместить в любом месте памяти без изменения кода. Но если я правильно понимаю, согласно Википедии, relocation динамический компоновщик может вносить исправления во время выполнения, поэтому символ может быть расположен, хотя код не является независимым от позиции. Но, согласно многим ответам, которые я видел здесь, не-pic код не может ASLR разделов, кроме стека (поэтому нельзя рандомизировать точку входа в программу). Если это правильно, то для чего используются исправления во время выполнения и почему мы не можем просто исправить все местоположения в коде во время выполнения перед запуском программы, чтобы сделать точку входа в программу случайной.


person Khaled    schedule 07.10.2020    source источник


Ответы (2)


TL:DR: не все варианты использования абсолютного адреса будут иметь информацию о перемещении в исполняемом файле, отличном от PIE (ELF типа EXEC, а не DYN). Поэтому загрузчик программы ядра не может найти их все для применения исправления.

Таким образом, нет возможности задним числом включить ASLR для исполняемых файлов, созданных не как PIE. У традиционного исполняемого файла нет возможности пометить себя как имеющую метаданные перемещения для каждого использования абсолютного адреса, и нет смысла добавлять такую ​​​​функцию, поскольку, если вам нужен текстовый ASLR, вы просто создадите PIE.

Поскольку исполняемые файлы ELF-типа EXEC Linux гарантированно загружаются/сопоставляются по фиксированному базовому адресу, выбранному компоновщиком во время компоновки, создание записей в таблице символов для внутренних символов было бы пустой тратой места в исполняемом файле. Так что тулчейны этого не сделали, и нет причин начинать. Просто так были разработаны традиционные исполняемые файлы ELF; Linux переключился с a.out на ELF еще в середине 90-х, до того, как стек ASLR стал чем-то особенным, поэтому он не был на радарах людей.

например абсолютный адрес static char buf[100], вероятно, встроен где-то в машинный код, который его использует (если мы говорим о 32-битном коде или 64-битном коде, который помещает адрес в регистр), но нет никакого способа узнать, где или сколько раз.

Кроме того, специально для x86-64 модель кода по умолчанию для исполняемых файлов, отличных от PIE, гарантирует, что все статические адреса (текст/данные/bss) будут находиться в нижних 2 ГБ виртуального адресного пространства, поэтому 32-разрядные абсолютные подписанные или неподписанные адреса могут работать, а rel32 смещения могут достигать чего угодно от чего угодно. Вот почему выходные данные компилятора, отличного от PIE, используют mov $symbol, %edi (5 байтов) для помещения адреса в регистр вместо lea symbol(%rip), %rdi (7 байтов). https://godbolt.org/z/89PeK1

Таким образом, даже если бы вы знали, где находится каждый абсолютный адрес, вы могли бы использовать ASLR только в младших 2 ГБ, ограничивая количество битов энтропии, которое вы могли бы ввести. (Я думаю, что в Windows есть режим для этого: LargeAddressAware = no. Но в Linux нет. -in-x86-64-linux">32-битные абсолютные адреса больше не разрешены в x86-64 Linux? Опять же, PIE - лучший способ разрешить текстовый ASLR, поэтому люди (дистрибутивы) должны просто компилировать для этого если они хотят его преимущества.)

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

При этом GNU/Linux поддерживает исправления исправлений для 64-разрядных абсолютных адресов даже в общих объектах PIC/PIE ELF. Вот почему код для начинающих, такой как NASM mov rdi, BUFFER, может работать даже в общей библиотеке: используйте objdump -drwC -Mintel, чтобы увидеть информацию о перемещении при использовании этого символа в инструкции mov reg, imm64. lea rdi, [rel BUFFER] не нуждалась бы в записи о перемещении, если бы BUFFER не был глобальным символом. (Эквивалент C static.)


Вам может быть интересно, почему метаданные необходимы:

Не существует надежного способа поиска в тексте/данных возможных абсолютных адресов; возможны ложные срабатывания. например /usr/bin/ld, вероятно, содержит 0x401000 в качестве начального адреса по умолчанию для исполняемого файла x86-64. Вы не хотите, чтобы ASLR кода + данных ld также изменил свои значения по умолчанию. Или это целочисленное значение могло появиться во многих программах любым количеством способов, например. как растровое изображение. И, конечно же, машинный код x86-64 имеет переменную длину, поэтому нет надежного способа даже отличить коды операций от непосредственных операндов в самом общем случае.

А также потенциально ложноотрицательные результаты. Маловероятно, что программа x86 создаст абсолютный адрес в регистре с несколькими инструкциями, но это, безусловно, возможно. Однако в коде, отличном от x86, это было бы обычным явлением.

Машины RISC с инструкциями фиксированной длины не могут поместить 32-битный адрес в 32-битную инструкцию; ни для чего другого не осталось бы места. Таким образом, для загрузки из статического хранилища абсолютные адреса должны быть разделены на несколько инструкций, например MIPS lui $t0, %hi(0x612300)/lw $t1, %lo(0x612300)($t0) для загрузки из статической переменной по абсолютному адресу 0x612300. (Обычно в исходном коде на ассемблере должно быть имя символа, но оно не будет отображаться в окончательном связанном двоичном файле, если только оно не будет .globl, поэтому я использовал числа в качестве напоминания.) Подобные инструкции не обязательно должны идти парами; та же старшая половина адреса может быть повторно использована другими обращениями к тому же массиву или структуре в последующих инструкциях.

person Peter Cordes    schedule 07.10.2020
comment
У меня создалось впечатление, что люди считали перемещение необходимым злом в 16-битных системах без виртуальной памяти, где все программы использовали одно и то же адресное пространство, и с благодарностью отказались от его поддержки с переходом на 32-битные системы, где программы всегда могли быть удалены. загружается по постоянному виртуальному адресу. Как вы говорите, полезность ASLR не предвиделась. - person Nate Eldredge; 08.10.2020
comment
@NateEldredge: Да, это хороший способ взглянуть на основной исполняемый файл. Совместно используемые библиотеки по-прежнему нуждаются либо в перемещении, либо в независимом от позиции коде, потому что только основной исполняемый файл может гарантировать свое место в виртуальном адресном пространстве. (IIRC, библиотеки Linux a.out нужно перемещать вручную, и вы могли бы оптимизировать свои общие библиотеки или что-то еще, заблаговременно переместив их на неконфликтующие базовые адреса. Или, может быть, я Я думаю о DLL?Я начал использовать Linux вскоре после того, как ELF заменил a.out, поэтому я читал об этом переходе, но никогда с ним не сталкивался). - person Peter Cordes; 08.10.2020
comment
@NateEldredge: также обратите внимание, что общие объекты Linux ELF do поддерживают исправления во время выполнения, часто используемые для таблицы переходов абсолютных адресов в качестве данных. Но да, 32-битный PIC имеет значительные накладные расходы из-за отсутствия адресации относительно EIP, поэтому исправления были бы значительно более производительными для общих библиотек. Но тогда они не могли обмениваться текстом между процессами, и их загрузка занимала больше времени. Системы Unix часто запускают множество недолговечных процессов. - person Peter Cordes; 08.10.2020

Давайте сначала посмотрим на Windows, прежде чем смотреть на Linux:

Файлы (программы) Windows .EXE обычно имеют так называемую базовую таблицу перемещения и у них есть база образов.

База образа — желаемый начальный адрес программы; если Windows загружает программу по этому адресу, перемещение не требуется.

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

Если файл .EXE не содержит базовой таблицы релокации (насколько мне известно, некоторые 32-битные версии GCC генерируют такие файлы), загрузить файл по другому адресу невозможно.

Это связано с тем, что следующие операторы кода C приведут к точно такому же машинному коду (двоичному коду), если переменная someVariable расположена по адресу 12340000, и их невозможно различить:

long myVariable = 12340000;

А также:

int * myVariable = &someVariable;

В первом случае значение 12340000 менять нельзя ни в коем случае; во втором случае адрес (а это 12340000) надо поменять на реальный адрес, если программа загружается по другому адресу.

Если базовая таблица перемещений отсутствует, отсутствует информация о том, является ли значение 12340000 целым числом (которое нельзя изменять) или адресом (который необходимо изменить).

Таким образом, программа должна быть загружена по какому-то фиксированному адресу.

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

Я не знаю насчет 64-битных Linux-программ, но если программа скомпилирована так же, как и (более старые) 32-битные программы, они также должны быть загружены по определенному адресу, и ASLR невозможен.

person Martin Rosenau    schedule 07.10.2020