Я понимаю, что код PIC делает рандомизацию ASLR более эффективной и простой, поскольку код можно разместить в любом месте памяти без изменения кода. Но если я правильно понимаю, согласно Википедии, relocation динамический компоновщик может вносить исправления во время выполнения, поэтому символ может быть расположен, хотя код не является независимым от позиции. Но, согласно многим ответам, которые я видел здесь, не-pic код не может ASLR
разделов, кроме стека (поэтому нельзя рандомизировать точку входа в программу). Если это правильно, то для чего используются исправления во время выполнения и почему мы не можем просто исправить все местоположения в коде во время выполнения перед запуском программы, чтобы сделать точку входа в программу случайной.
почему код без pic не может быть полностью ASLR с помощью исправлений во время выполнения?
Ответы (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
, поэтому я использовал числа в качестве напоминания.) Подобные инструкции не обязательно должны идти парами; та же старшая половина адреса может быть повторно использована другими обращениями к тому же массиву или структуре в последующих инструкциях.
Давайте сначала посмотрим на 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 невозможен.