【问题标题】:Symbol addresses during load-time linking vs run-time linking in LinuxLinux 中加载时链接与运行时链接期间的符号地址
【发布时间】:2017-06-08 06:48:32
【问题描述】:

我试图了解 Linux 中动态库的加载时链接(使用 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编译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 的版本中。有没有办法在第二次加载时保持这种状态?

【问题讨论】:

  • 相关,请参阅Configuring ASLR with randomize_va_spaceJump Over ASLR: Attacking Branch Predictors to Bypass ASLR。如果你想随机化数据段,那么我相信你需要设置randomize_va_space = 2
  • 在linux/amd64中,我得到了类似的结果;注意:我使用此命令链接可执行文件:gcc -o main main.c -L. -lhello -ldl -Wl,-rpath $(pwd -L)
  • 也在 AIX/PowerPC 中尝试过,两个地址对('func' 和 'var')是相等的。链接命令是:gcc -o main main.c -L. -lhello -ldl -Wl,-brtl,-blibpath:$(pwd -L):/usr/lib

标签: c++ c linux gcc dynamic-linking


【解决方案1】:
  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

【讨论】:

  • 这比我预期的要微妙得多。感谢您的澄清。 RTLD_DEFAULT 没有广泛记录,解析加载时变量的 dlopen() 函数也是令人惊讶的行为。这个答案对两者都有帮助。
【解决方案2】:

您所看到的取决于许多变量。这是我第一次尝试的 Debian 64 位系统

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】:

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

    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:~>
    

    【讨论】:

      【解决方案4】:

      在我看来,我会说:

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

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

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

      【讨论】:

      • 请注意,OP 在这里不执行任何静态链接,他以 2 种不同的方式链接到共享库。
      • 是的,你当然是对的。在这里留下我的答案,因为它可能对其他人有帮助。
      猜你喜欢
      • 2015-05-05
      • 2015-07-31
      • 2011-01-04
      • 1970-01-01
      • 2010-10-04
      • 1970-01-01
      • 1970-01-01
      • 2016-12-12
      • 2010-12-01
      相关资源
      最近更新 更多