Пример минимального запуска: 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:
main.S
.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 upstream
и он собирается и работает нормально:
as -o main.o main.S
ld -o main.out main.o
./main.out
Однако, если мы попытаемся связать его как пирог с:
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
и поэтому не зависит от позиции.
--no-dynamic-linker
требуется, как объяснено в: Как создать статически связанный независимый от позиции исполняемый файл ELF в Linux?
Если мы вместо этого напишем это независимо от позиции:
lea msg(%rip), %rsi
тогда PIE link работает нормально, и 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 .
Как определить, является ли ELF независимым от позиции?
Помимо простого запуска через GDB, некоторые статические методы упоминаются в:
Проверено в Ubuntu 18.10.