Символьные адреса во время привязки времени загрузки и времени выполнения в 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), представляют собой совершенно разные переменные (каждый из которых загружен в другом месте памяти), поэтому они имеют разные значения и не взаимодействуют.