奇幻LD_PRELOAD环境变量


虽然从每个人的角度来看,似乎并不是所有人都喜欢完全控制自己的操作系统,但是对于高级玩家来说,能够控制控制自己操作系统的方方面面总是一件令人愉快的事情。

奇幻LD_PRELOAD环境变量


1. 问题起因

升级/更新需谨慎,否则亲人两行泪!

  • 今天上班的时候有同事反馈了一个 Python 包使用的问题,因为在该服务器(Ubuntu14.04)上使用该安装包,需要依赖于 gcc5 的版本,但是系统自带的是 gcc4 的版本,所以需要升级。

  • 还有就是 apt update 时候,一定要注意 remove 的包,包冲突之后 apt 就帮我把很多必须的包给删除了,不然升级之后系统可能无法使用了。如果提示删除包的时候,我们没有注意删除的包,然后就只剩恢复系统了,哈哈哈。

  • 但是通过源码方式替代系统 gcc4 版本,需要时用 ln 命令将其链接地址改到 libc.so.6 版本 so 的位置。但是执行该命令的时候,链接错了地址,导致整个系统几乎所有的命令都无法执行,除了 exportenv 等命令可以使用。因为在 Linux 中命令基本都用到了 glibc 库,生成了有一个叫 libc.so.6 的文件,这是几乎所有 Linux 下命令的动态链接中,其中有标准 C 的各种函数。

奇幻LD_PRELOAD环境变量


2. 处理经过

  • 执行完之后,感觉心里一紧。其实执行之前就知道有可能会出现上面这种,所有命令都无法使用的近期情况,但是还是没有做好操作失败之后的修复方式。因为几乎所有的命令都无法使用,所有也没有办法重新更改 link 文件的链接地址了。
# 我们可以看到命令都是需要动态链接的
$ ldd /usr/bin/lsof
    linux-vdso.so.1 (0x00007ffead5d4000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f64feb94000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f64fe7f5000)
    libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f64fe582000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f64fe37e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f64fefe4000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f64fe161000)
  • 该服务器上面还是很多开发和测试环境使用的环境和配置的程序,如果正式的导致系统无法使用,那就玩大的。所有开始在网上找相关的内容,可是一直都没有知道处理上述问题的方法,让人感觉到绝望。
  • 幸好,有个厉害的同事说可以使用 LD_PRELOAD 环境变量来优先加载指定 so 文件,这样命令就可以使用,对原本有问题的 link 文件进行修复了。随即,试了下,果然奏效了。
LD_PRELOAD=/lib/x86_64-linux-gnu/libc-2.19.so ln -sf xxx xxx

3. 补充知识

**LD_PRELOAD**是个环境变量,用于动态库的加载,动态库加载的优先级最高。

[1] 动态库加载顺序

  • 1. LD_PRELOAD
  • 2. LD_LIBRARY_PATH
  • 3. /etc/ld.so.cache
  • 4. /lib
  • 5. /usr/lib

[2] 程序的链接

  • 所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置。一般来说,程序的链接分为静态链接动态链接静态链接就是把所有所引用到的函数或变量全部地编译到可执行文件中,动态链接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数库,也就是运行链接。
  • 所以,对于动态链接来说,必然需要一个动态链接库。动态链接库的好处在于,一旦动态库中的函数发生变化,对于可执行程序来说是透明的,可执行程序无需重新编译。这对于程序的发布、维护、更新起到了积极的作用。对于静态链接的程序来说,函数库中一个小小的改动需要整个程序的重新编译、发布,对于程序的维护产生了比较大的工作量。
  • 当然,世界上没有什么东西都是完美的,有好就有坏,有得就有失。动态链接 所带来的坏处和其好处一样同样是巨大的。因为程序在运行时动态加载函数,这也就为他人创造了可以影响你的主程序的机会。试想,一旦你的程序动态载入的函数不是你自己写的,而是载入了别人的有企图的代码,通过函数的返回值来控制你的程序的执行流程。那么,你的程序也就被人 劫持 了。

[3] 简介 LD_PRELOAD 环境变量

  • UNIX 的动态链接库的世界中,LD_PRELOAD 就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker)它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。
  • 通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入恶意程序,从而达到那不可告人的罪恶的目的。
  • 我们知道 Linux 中命令基本都用到了 glibc 库,生成了有一个叫 libc.so.6 的文件,这是几乎所有 Linux 下命令的动态链接中,其中有标准 C 的各种函数。对于 GCC 而言,默认情况下,所编译的程序中对标准 C 函数的链接,都是通过动态链接方式来链接 libc.so.6 这个函数库的。

4. 原理说明

  • 做一个简单的实验,在不使用任何参数的情况下,编译如下 C 代码。编译命令是这样的 gcc random_num.c -o random_num,希望得到的结果是 0-99 中的十个随机数,且每次谁出的结果都不同。我们可以通过编译后的 random_num 函数,来得到我们想要的随机数了。
##############
# random_num.c
##############

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(){
  srand(time(NULL));
  int i = 10;
  while(i--) printf("%d\n",rand()%100);
  return 0;
}
  • 我们使用这条命令 gcc -shared -fPIC unrandom.c -o unrandom.so 将另一个 C 程序编译进入一个共享库中,它使用一个常数值 42 实现了一个 rand() 函数。因此,现在我们已经有了一个可以输出一些随机数的应用程序和一个定制的库
##############
# unrandom.c
##############

int rand(){
    return 42; //the most random number in the universe
}
  • 现在,我们再试用如下命令执行程序的时候,发现已经的不要我们原有的十个随机数了,而是返回十次常数 42 了。这是为什么呢?一个未被改变过的应用程序在一个正常的运行方式中,看上去受到了我们做的一个极小的库的影响。
# 指定的链接库只对后面的命令生效
$ LD_PRELOAD=$PWD/unrandom.so ./random_nums

# 定义环境变量则对后续命令都生效
$ export LD_PRELOAD=$PWD/unrandom.so
  • 是的,你猜对了。我们的程序生成随机数失败是因为它并没有使用真正的 rand() 函数,而是使用了我们提供的的那个每次都返回 42 的定制库。这时就需要我们通过 ldd 命令查看都多了些什么呢?
  • 下面有一个 libc.soso 文件,这个文件提供了核心的 C 函数,其中就包含了 真正的 rand() 函数。但是,我们通过使用 LD_PRELOAD 环境变量之后,为这个程序强制加载一些库(它为 random_num 应用程序加载了 unrandom.so库)。所以,我们可以知道其实程序真正执行的是我们指定库中的 rand() 函数,返回的是常数 42 了。
# 程序random_nums所需要的库的列表
# 这个列表是构建进可执行程序中的并且它是在编译时决定的
$ ldd random_nums
linux-vdso.so.1 => (0x00007fff4bdfe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000)

# 可以使用如下命令查看so文件的全部函数列表
$ nm -D /lib/libc.so.6 | grep "rand()"
# 列出我们强加指定库之后的链接库列表
$ LD_PRELOAD=$PWD/unrandom.so ldd random_nums
linux-vdso.so.1 =>  (0x00007fff369dc000)
/some/path/to/unrandom.so (0x00007f262b439000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f262b044000)
/lib64/ld-linux-x86-64.so.2 (0x00007f262b63d000)
  • 需要注意的是,别以为通过 LD_PRELOAD 环境变量就可以获得 root 权限了。你想多了,因为通过这种方法是不会绕过安全机制的限制的。专业的解释是,如果 RUID != EUID 的话,库是不会通过这种方法预加载进来的。
编号 系统权限 解释说明
1 RUID 用于在系统标识用户;当用户使用用户名和密码成功登录后确定了它的 RUID
2 EUID 用于系统决定用户对系统资源的访问权限;通常情况下等于 RUID 相同
3 SUID 用于对外权限的开放;与 RUID 及 EUID 不同,它是跟文件而不是跟用户进行绑定

5. 总结回顾

运用 LD_PRELOAD 环境变量的总结

  1. 定义与目标函数完全一样的函数,包括名称、变量及类型、返回值及类型等
  2. 将包含替换函数的源码编译为动态链接库
  3. 通过命令 export LD_PRELOAD="库文件路径",设置要优先替换动态链接库
  4. 如果找不到替换库,通过 export LD_LIBRARY_PATH=库文件目录路径 设置系统查找库的目录
  5. 替换结束,要还原函数调用关系,用命令 unset LD_PRELOAD 解除
  6. 想查询依赖关系,可以用 ldd 程序名称 查看

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录