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

Symbol addresses during load-time linking vs run-time linking in Linux

我试图了解 运行 中动态库的加载时链接(使用 gcc -l)与 运行 时链接(使用 dlopen(), dlsym())机制的区别=64=],以及这些机制如何影响库的状态及其符号的地址。

实验

我有三个简单的文件:

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

编译libhello.c

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

编译 main.c

观察

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

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

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

问题

  1. 为什么 运行 时间的地址每隔 运行 就改变一次?它们是否因 Address space layout randomization 而改变?
  2. 如果是这种情况,为什么加载时链接的地址不发生变化?加载时链接是否容易受到解决随机化旨在防止的相同攻击?
  3. 在上面的程序中,同一个库被加载了两次 - 一次在加载时,然后在 运行 时使用 dlopen()。第二个加载不复制第一个加载的状态。 IE。如果 var 的值在 dlopen() 之前更改,则此值不会反映在通过 dlsym() 加载的 var 版本中。有没有办法在第二次加载时保持这个状态?

在我看来,我会说:

  • 当你直接用可执行文件(静态链接)编译库时,就好像函数会被直接注入到源代码中一样。如果您检查可执行文件,您会发现每个部分(代码、数据...)都有一个固定的 "virtual memory" 地址。如果我没记错的话,每个 Linux 可执行文件都将从默认地址 0x100000 开始,因此您会看到每个静态链接函数都有一个固定地址(0x100000 + 固定偏移量),并且永远不会改变。每次加载可执行文件时,每个特定函数都将加载到 "virtual memory" 中的那个精确地址,这意味着 OS 将决定使用哪个物理地址,但你不会看到它。在您的示例中,var 变量的虚拟地址始终为 0x0000000000601060,但您永远不知道它在物理内存中的位置。

  • 当您在 运行 加载动态库时,OS 已经将可执行文件加载到内存中,因此您将没有虚拟固定地址。相反,OS 在可执行地址 space 中保留了一个从 0x00007fxxxxxxxxxx 开始的虚拟地址范围,它将加载和映射新加载的符号和函数。根据已经加载的内容和内存随机化算法,这些地址在每个 运行.

  • 中可能不同

根据这个简短的解释,很容易假设您在第 3 点中比较的两个值是完全不同的变量(每个变量都加载到不同的内存位置),因此它们具有不同的值并且不互动。

您看到的内容取决于许多变量。在 Debian 64bit 上,我第一次尝试

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 return OS ad runtime 中的symbol address without relocation,这个地址在SO mapping region.

并且您可以使用映射信息来查找不同的:

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:~>
  1. 是的,是 ASLR。

  2. 因为 PIE(位置独立可执行文件)非常昂贵(在性能上)。如此多的系统在随机化库的地方进行权衡,因为无论如何它们都必须独立于位置,但不要随机化可执行文件,因为它会花费太多性能。是的,这种方式更容易受到攻击,但大多数安全性都是一种权衡。

  3. 是的,不要通过句柄搜索符号,而是使用 RTLD_DEFAULT。像这样加载同一个动态库的两个实例通常不是一个好主意。如果某些系统知道已经加载了相同的库并且动态链接器认为 "the same library" 可以根据您的库路径更改,则某些系统可以跳过加载 dlopen 中的库。您在很大程度上处于完全 badly/weakly 定义的行为领域,这些行为多年来已经演变为更多地处理错误和问题,而不是通过深思熟虑的设计。

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

此外,另一件值得记住的事情是,如果您在 libhello 中引用 var,即使在 dlopen:ed 版本中,它也会始终从库的加载时间版本解析符号。我将 func 修改为 return 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