在Linux加载时链接与运行时链接期间的符号地址

我试图理解Linux中dynamic库加载时间链接(使用gcc -l )和运行时链接(使用dlopen(), dlsym() )的机制的差异,以及这些机制如何影响图书馆和其符号的地址。

本实验

我有三个简单的文件:

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")); } 

我使用gcc -shared -o libhello.so -fPIC libhello.c编译gcc -shared -o libhello.so -fPIC libhello.c

我使用gcc main.c -L. -lhello -ldl命令gcc main.c -L. -lhello -ldl gcc main.c -L. -lhello -ldl

观察

运行main.c可执行文件打印如下:

 Address Load-time linking Run-time linking ------- ----------------- ---------------- &var 0x0000000000601060 0x00007fdb4acb1034 &func 0x0000000000400700 0x00007fdb4aab0695 

加载时链接地址保持不变,但运行时链接地址每次都会更改。

问题

  1. 为什么运行时地址每次运行都会改变? 地址空间布局随机化,他们改变吗?
  2. 如果是这种情况,为什么不加载地址更改加载时间链接? 加载时间链接是否容易遭受与随机化旨在防范相同的攻击?
  3. 在上面的程序中,同一个库被加载了两次 – 一次是在加载时,然后是在运行时使用dlopen() 。 第二个负载不复制第一个负载的状态。 也就是说,如果var的值在dlopen()之前更改,则此值不会反映在通过dlsym()加载的var版本中。 第二次装载时有没有办法保留这个状态?

  1. 是的,这是ASLR。

  2. 因为PIE(位置独立可执行文件)相当昂贵(在性能上)。 所以很多系统都是在随机化库的情况下进行权衡,因为它们必须独立于位置,但不要随机化可执行文件,因为它的性能太高。 是的,这种方式更容易受到攻击,但大多数安全性是一个折衷。

  3. 是的,不要通过句柄搜索符号,而是使用RTLD_DEFAULT 。 如此加载相同动态库的两个实例通常是一个糟糕的主意。 有些系统可以跳过在dlopen加载一个库,如果他们知道同一个库已经被加载的话,那么动态链接器认为“同一个库”可​​以改变,这取决于你的库路径。 你们非常处于相当严重/软弱行为的境地,这些行为多年来一直在演变,以处理错误和问题,而不是通过有意的设计。

请注意, RTLD_DEFAULT将返回主可执行文件或第一个(加载时间)加载的动态库中的符号地址,并且动态加载的库将被忽略。

另外值得一提的是,如果你在libhello中引用var ,那么即使在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 

你所看到的取决于许多变数。 在我的第一次尝试中,在Debian 64位上

 Address Load-time linking Run-time linking ------- ----------------- ---------------- &var 0x0000000000600d58 0x0000000000600d58 &func 0x00000000004006d0 0x00000000004006d0 

这意味着,dlopen使用已经链接的库,你的系统似乎不这么做。 为了获得ASLR的好处,你需要用位置独立代码编译main.cgcc -fPIC main.c ./libhello.so -ldl

 Address Load-time linking Run-time linking ------- ----------------- ---------------- &var 0x00007f4e6cec6944 0x00007f4e6cec6944 &func 0x00007f4e6ccc6670 0x00007f4e6ccc6670 

我希望这个提示可以帮助你。

  1. 主程序是ELF文件,需要重定位。 重新定位发生在加载时间。 因此,在调用dlsym之前,主程序中的var和func地址已经重新定位。

  2. dlsym func在OS广告运行时返回符号地址而不进行重定位,该地址位于SO映射区域中。

你可以使用映射信息来找到不同的地方:

 wutiejun@linux-00343520:~/Temp/sotest> LD_LIBRARY_PATH=./ ./test Address Load-time linking Run-time linking ------- ----------------- ---------------- &var 0x000000000804a028 0x00000000f77a9014 &func 0x0000000008048568 0x00000000f77a744c wutiejun@linux-00343520:~> 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] wutiejun@linux-00343520:~> 

在我看来,我会说:

  • 当你用可执行文件(静态链接)直接编译库时,就好像这些函数会被直接注入源代码一样。 如果您检查可执行文件,您会看到每个部分(代码,数据…)将具有固定的“虚拟内存”地址。 如果我没记错的话,每个Linux可执行文件都将从0x100000的默认地址开始,所以你会看到每个静态链接函数都有一个固定的地址(0x100000 +固定偏移量),并且永远不会改变。 每次加载可执行文件时,每个特定的函数都将被加载到“虚拟内存”中的精确地址,这意味着操作系统将决定使用哪个物理地址,但是您不会看到这个地址。 在你的例子中, var变量总是具有0x0000000000601060的虚拟地址,但你永远不会知道它将驻留在物理内存中的什么位置。

  • 当您在运行时加载一个动态库时,操作系统已经将可执行文件加载到内存中,因此您将不会拥有一个虚拟的固定地址。 相反,操作系统会在可执行地址空间中保留一个从0x00007fxxxxxxxxxx开始的虚拟地址范围,它将加载并映射新加载的符号和函数。 根据已经加载的内存和内存随机算法,这些地址在每次运行中可能不同。

给出这个简短的解释,很容易假设你在3)中比较的两个值是完全不同的变量(每个变量都加载在不同的内存位置),因此它们具有不同的值并且不会相互影响。