Символьные адреса во время привязки времени загрузки и времени выполнения в Linux
Я пытаюсь понять разницу в механизмах, лежащих в основе привязки времени загрузки (используя gcc -l
) по сравнению с привязкой во время выполнения (используя dlopen(), dlsym()
) динамических библиотек в Linux и как эти механизмы влияют на состояние библиотеки и адреса его символов.
Эксперимент
У меня есть три простых файла:
libhello.c:
int var;
int func() {
return 7;
}
libhello.h:
extern int var;
int func();
main.c:
#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
#include "libhello.h"
int main() {
void* h = dlopen("libhello.so", RTLD_NOW);
printf("Address Load-time linking Run-time linking\n");
printf("------- ----------------- ----------------\n");
printf("&var 0x%016" PRIxPTR " 0x%016" PRIxPTR "\n", (uintptr_t)&var , (uintptr_t)dlsym(h, "var" ));
printf("&func 0x%016" PRIxPTR " 0x%016" PRIxPTR "\n", (uintptr_t)&func, (uintptr_t)dlsym(h, "func"));
}
Я компилирую libhello.c командой gcc -shared -o libhello.so -fPIC libhello.c
Я компилирую main.c командой gcc main.c -L. -lhello -ldl
Наблюдение
Запуск исполняемого файла main.c печатает примерно так:
Address Load-time linking Run-time linking
------- ----------------- ----------------
&var 0x0000000000601060 0x00007fdb4acb1034
&func 0x0000000000400700 0x00007fdb4aab0695
Адресаты ссылок на загрузку остаются неизменными, но адреса ссылок во время выполнения меняют каждый прогон.
Вопросы
- Почему во время запуска меняются адреса времени выполнения? Изменяются ли они из-за рандомизация размещения адресного пространства?
- Если это так, почему адреса не меняются для привязки времени загрузки? Является ли привязка времени загрузки уязвимой к тем же атакам, которые направлены на рандомизацию для защиты от?
- В приведенной выше программе одна и та же библиотека загружается дважды - один раз во время загрузки, а затем во время выполнения с помощью
dlopen()
. Второй груз не копирует состояние первой загрузки. То есть если значение var
изменено до dlopen()
, это значение не отражается в версии var
, загруженной через dlsym()
. Есть ли способ сохранить это состояние во время второй загрузки?
Ответы
Ответ 1
-
Да, это ASLR.
-
Поскольку PIE (независимые от позиции) довольно дороги (в производительности). Так много систем делают компромисс, где они рандомизируют библиотеки, потому что они должны быть независимыми по позиции в любом случае, но не рандомизируйте исполняемые файлы, потому что это стоит слишком большой производительности. Да, он более уязвим таким образом, но большая часть безопасности - это компромисс.
-
Да, не просматривайте символы через дескриптор, вместо этого используйте RTLD_DEFAULT
. Как правило, плохая идея иметь два экземпляра одной и той же динамической библиотеки, загруженной таким образом. Некоторые системы могут просто пропустить загрузку библиотеки в dlopen
, если они знают, что одна и та же библиотека уже загружена, и то, что динамический компоновщик считает "той же библиотекой", может меняться в зависимости от вашего пути к библиотеке. Вы в значительной степени находитесь на территории довольно плохо/слабо определенного поведения, которое на протяжении многих лет эволюционировало, чтобы справляться с ошибками и проблемами, а тем более с помощью преднамеренного проектирования.
Обратите внимание, что RTLD_DEFAULT
вернет адрес символа в основном исполняемом файле или в первую (загруженную) загруженную динамическую библиотеку, и динамически загруженная библиотека будет проигнорирована.
Кроме того, стоит иметь в виду, что если вы ссылаетесь на var
в libhello, он всегда будет разрешать символ из версии времени загрузки библиотеки даже в версии dlopen: ed. Я изменил func
, чтобы вернуть var
, и добавил этот код к вашему примеру:
int (*fn)(void) = dlsym(h, "func");
int *vp;
var = 17;
printf("%d %d %d %p\n", var, func(), fn(), vp);
vp = dlsym(h, "var");
*vp = 4711;
printf("%d %d %d %p\n", var, func(), fn(), vp);
vp = dlsym(RTLD_DEFAULT, "var");
*vp = 42;
printf("%d %d %d %p\n", var, func(), fn(), vp);
и получить этот вывод:
$ gcc main.c -L. -lhello -ldl && LD_LIBRARY_PATH=. ./a.out
17 17 17 0x7f2e11bec02c
17 17 17 0x7f2e11bec02c
42 42 42 0x601054
Address Load-time linking Run-time linking
------- ----------------- ----------------
&var 0x0000000000601054 0x0000000000601054
&func 0x0000000000400700 0x0000000000400700
Ответ 2
То, что вы видите, зависит от многих переменных. Здесь, на 64-битном Debian, я получил первую попытку
Address Load-time linking Run-time linking
------- ----------------- ----------------
&var 0x0000000000600d58 0x0000000000600d58
&func 0x00000000004006d0 0x00000000004006d0
Это означает, что dlopen использовал уже связанную библиотеку, которую, похоже, не делает ваша система. Чтобы получить преимущество ASLR, вам нужно скомпилировать main.c
с независимым положением: gcc -fPIC main.c ./libhello.so -ldl
.
Address Load-time linking Run-time linking
------- ----------------- ----------------
&var 0x00007f4e6cec6944 0x00007f4e6cec6944
&func 0x00007f4e6ccc6670 0x00007f4e6ccc6670
Ответ 3
Надеюсь, этот намек поможет вам.
-
Основная программа - это файл ELF, и ему необходимо переместить. И перемещение происходит во время загрузки. Таким образом, адрес var и func в основной программе переместился, прежде чем вы вызовете dlsym.
-
dlsym func возвращает адрес символа в рабочей среде OS без переезда, этот адрес находится в области отображения SO.
И вы можете использовать информацию сопоставления, чтобы найти разные:
[email protected]:~/Temp/sotest> LD_LIBRARY_PATH=./ ./test
Address Load-time linking Run-time linking
------- ----------------- ----------------
&var 0x000000000804a028 0x00000000f77a9014
&func 0x0000000008048568 0x00000000f77a744c
[email protected]:~> cat /proc/7137/maps
08048000-08049000 r-xp 00000000 08:02 46924194 /home/wutiejun/Temp/sotest/test
08049000-0804a000 r--p 00000000 08:02 46924194 /home/wutiejun/Temp/sotest/test
0804a000-0804b000 rw-p 00001000 08:02 46924194 /home/wutiejun/Temp/sotest/test
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
f75d3000-f7736000 r-xp 00000000 08:02 68395411 /lib/libc-2.11.3.so
f7736000-f7738000 r--p 00162000 08:02 68395411 /lib/libc-2.11.3.so
f7738000-f7739000 rw-p 00164000 08:02 68395411 /lib/libc-2.11.3.so
f7739000-f773c000 rw-p 00000000 00:00 0
f773c000-f7740000 r-xp 00000000 08:02 68395554 /lib/libachk.so
f7740000-f7741000 r--p 00003000 08:02 68395554 /lib/libachk.so
f7741000-f7742000 rw-p 00004000 08:02 68395554 /lib/libachk.so
f777a000-f777c000 rw-p 00000000 00:00 0
f777c000-f7784000 r-xp 00000000 08:02 68395441 /lib/librt-2.11.3.so
f7784000-f7785000 r--p 00007000 08:02 68395441 /lib/librt-2.11.3.so
f7785000-f7786000 rw-p 00008000 08:02 68395441 /lib/librt-2.11.3.so
f7786000-f779d000 r-xp 00000000 08:02 68395437 /lib/libpthread-2.11.3.so
f779d000-f779e000 r--p 00016000 08:02 68395437 /lib/libpthread-2.11.3.so
f779e000-f779f000 rw-p 00017000 08:02 68395437 /lib/libpthread-2.11.3.so
f779f000-f77a2000 rw-p 00000000 00:00 0
f77a2000-f77a5000 r-xp 00000000 08:02 68395417 /lib/libdl-2.11.3.so
f77a5000-f77a6000 r--p 00002000 08:02 68395417 /lib/libdl-2.11.3.so
f77a6000-f77a7000 rw-p 00003000 08:02 68395417 /lib/libdl-2.11.3.so
f77a7000-f77a8000 r-xp 00000000 08:02 46924193 /home/wutiejun/Temp/sotest/libhello.so
f77a8000-f77a9000 r--p 00000000 08:02 46924193 /home/wutiejun/Temp/sotest/libhello.so
f77a9000-f77aa000 rw-p 00001000 08:02 46924193 /home/wutiejun/Temp/sotest/libhello.so
f77aa000-f77ab000 rw-p 00000000 00:00 0
f77ab000-f77ca000 r-xp 00000000 08:02 68395404 /lib/ld-2.11.3.so
f77ca000-f77cb000 r--p 0001e000 08:02 68395404 /lib/ld-2.11.3.so
f77cb000-f77cc000 rw-p 0001f000 08:02 68395404 /lib/ld-2.11.3.so
ffd99000-ffdba000 rw-p 00000000 00:00 0 [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
[email protected]:~>
Ответ 4
На мой взгляд, я бы сказал, что:
-
Когда вы компилируете библиотеку непосредственно с исполняемым файлом (статическая привязка), думайте так, как будто функции будут непосредственно вставляться в исходный код. Если вы изучите исполняемый файл, вы увидите, что каждый раздел (код, данные,...) будет иметь фиксированный адрес "виртуальной памяти". Если я хорошо помню, каждый исполняемый файл Linux будет начинаться с адреса по умолчанию 0x100000, поэтому вы увидите, что каждая статическая связанная функция будет иметь фиксированный адрес (0x100000 + фиксированное смещение), и это никогда не изменится. Каждый раз, когда исполняемый файл загружается, каждая конкретная функция будет загружена с точным адресом в "виртуальной памяти", что означает, что ОС решит, какой физический адрес будет использоваться, но вы этого не увидите. В вашем примере переменная var всегда будет иметь виртуальный адрес 0x0000000000601060, но вы никогда не узнаете, где находится физическая память.
-
Когда вы загружаете во время выполнения динамическую библиотеку, ОС уже загружает исполняемый файл в память, поэтому у вас не будет виртуального фиксированного адреса. Вместо этого ОС резервирует в исполняемом адресном пространстве диапазон виртуальных адресов, начиная с 0x00007fxxxxxxxxxx, где он загружает и отображает недавно загруженные символы и функции. В зависимости от того, что уже было загружено, и алгоритмов рандомизации памяти, эти адреса могут отличаться в каждом прогоне.
Учитывая это краткое объяснение, просто предположить, что два значения, которые вы сравниваете в своей точке 3), представляют собой совершенно разные переменные (каждый из которых загружен в другом месте памяти), поэтому они имеют разные значения и не взаимодействуют.