从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

一:概述
在Linux开发过程中,我们会遇到这样的情况,明明可执行程序elf与共享库so在同一个目录,但是进入此目录执行./elf 却会提示so找不到?这种情况对于从Windows平台过渡过来的程序员是比较费解的。这里其实就涉及到Linux上可执行程序搜索需要的库的一个范围和顺序问题。首先,是范围问题,我们可以通过查询ld的说明文档知晓范围。其次,是顺序问题,如果第三方程序制作了一个与系统库同名的so库,并把它放在了优先加载的位置,即优先于系统库目录的位置,定然就会发生劫持,这是一个风险点。
网络上比较流行的Linux动态库劫持的方式是通过/etc/ld.so.preload或者环境变量LD_PRELOAD来劫持的,这种方式很容易被发现,因为它会影响所有后续启动的进程,比如有些挖矿进程隐身的时候就使用了此技术,这种方式我们先不谈,本文,我们谈一谈库加载顺序可能导致的风险,这个风险不容易被发现,但值得警惕。

二:so共享库运行时加载顺序验证
1)准备
Linux共享库的运行时加载顺序为:
1:环境变量LD_LIBRARY_PATH指定的路径
2:连接时 -rpath指定的共享库查找路径
3:ldconfig 配置文件ld.so.conf指定的路径
4:/lib
5:/usr/lib
因为Windows搜索dll的路径会搜索本地目录和PATH路径,我们也捎带测试一下这两种情况。

在作者的机器上,/lib为/usr/lib的软链接,
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

所以,我们只测试前四种情况+PATH+本地!
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

为了公平验证所有运行时加载顺序,
我们在这几个目录都放置好同名但print输出不同的so库文件,-rpath指定好主程序的加载目录,配置好PATH路径,配置好ld.so.conf。运行主程序,查看输出结果,不断删除起作用的项,可逐一验证运行时加载顺序:

编写七个文件如下
user@kali:~/pro$ cat a1.c
#include <stdio.h>

void myprint()
{
printf("Hello a1[LD_LIBRARY_PATH]\n");
}
user@kali:~/pro$ cat a2.c
#include <stdio.h>

void myprint()
{
printf("Hello a2[rpath]\n");
}
user@kali:~/pro$ cat a3.c
#include <stdio.h>

void myprint()
{
printf("Hello a3[ld.so.conf]\n");
}
user@kali:~/pro$ cat a4.c
#include <stdio.h>

void myprint()
{
printf("Hello a4[/lib--/usr/lib]\n");
}
user@kali:~/pro$ cat a5.c
#include <stdio.h>

void myprint()
{
printf("Hello a5 PATH\n");
}
user@kali:~/pro$ cat a6.c
#include <stdio.h>

void myprint()
{
printf("Hello a6 local\n");
}
user@kali:~/pro$ cat main.c
#include <stdio.h>

extern void myprint();
int main()
{
myprint();
return 0;
}
编译和设置,步骤如下:
// 创建文件夹
user@kali:~/pro$ mkdir dir_ld_conf dir_path dir_ld_path dir_rpath
其中:
dir_ld_conf 用来验证ld.so.conf的作用
dir_ld_path 用来验证LD_LIBRARY_PATH的作用
dir_path 用来验证环境变量PATH的作用
dir_rpath 用来验证链接时rpath的作用

// a1.c 验证LD_LIBRARY_PATH
user@kali:~/pro$ gcc -shared -o ./dir_ld_path/liba.so a1.c
user@kali:~/pro$ export LD_LIBRARY_PATH=/home/user/pro/dir_ld_path
user@kali:~/pro$ echo $LD_LIBRARY_PATH
/home/user/pro/dir_ld_path

// a2.c 验证rpath
user@kali:~/pro$ gcc -shared -o ./dir_rpath/liba.so a2.c

// a3.c 验证ld.so.conf
user@kali:~/pro$ gcc -shared -o ./dir_ld_conf/liba.so a3.c
// 编辑,加入/home/user/pro/dir_ld_conf
user@kali:~/pro/dir_ld_conf$ sudo vim /etc/ld.so.conf
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险
// 刷新缓存
user@kali:~/pro/dir_ld_conf$ sudo ldconfig

// a4.c 验证/lib /usr/lib
user@kali:~/pro$ sudo gcc -shared -o /lib/liba.so a4.c
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

// a5.c 验证PATH
user@kali:~/pro$ gcc -shared -o ./dir_path/liba.so a5.c
user@kali:~/pro$ export PATH=/home/user/pro/dir_path:$PATH
user@kali:~/pro$ echo $PATH
/home/user/pro/dir_path:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

// a6.c 验证本地
user@kali:~/pro$ gcc -shared -o liba.so a6.c

// 编译主程序[带rpath]
user@kali:~/pro$ gcc -o main main.c -L. -la -Wl,-rpath,dir_rpath

此时目录结构如下:
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

我们看一下动态节.dynamic的内容:
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

可见rpath指定的路径已经被写入了可执行文件中。

2)手工验证
一切就绪,我们开始验证:
user@kali:~/pro$ ./main
Hello a1[LD_LIBRARY_PATH]
可见 LD_LIBRARY_PATH第一个起作用
删除dir_ld_path之后
user@kali:~/pro$ ./main
Hello a2[rpath]
可见-rpath连接选项第二个起作用
删除dir_rpath之后
user@kali:~/pro$ ./main
Hello a3[ld.so.conf]
可见ld.so.conf配置文件第三个起作用
删除 dir_ld_conf之后
user@kali:~/pro$ ./main
Hello a4[/lib--/usr/lib]
可见 /lib 目录生效,第四个起作用
删除/lib/liba.so之后
user@kali:~/pro$ ./main
./main: error while loading shared libraries: liba.so: cannot open shared object file: No such file or directory
可见只有这四个可以起作用,我们设置的PATH中的路径和本地路径都没有起作用。
如果感兴趣的话,也可以用strace追踪一下main的系统调用过程,也能发现调用过程是这样的顺序。
至此,可以得出结论:Linux的共享库so的寻找顺序为:
1:LD_LIBRARY_PATH
2:-rpath连接选项
3:ld.so.conf配置文件
4:/lib和/usr/lib

3)源代码验证
既然Linux是开源系统,最权威的验证方式,当然是源代码了,库的寻找顺序,系统是交给动态链接器来管理的,Linux的ELF动态链接器是Glibc的一部分,作者从glibc-2.29版本中的dl-load.c文件中找到这么一个函数记载了运行时加载顺序的寻找过程:
struct link_map
_dl_map_object (struct link_map
loader, const char *name,
int type, int trace_mode, int mode, Lmid_t nsid)
几个主要的代码片段如下:
从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

最开始是RPATH与RUNPATH同时存在时的处理方式,RPATH是旧式的编译器用的方式,RUNPATH是最新的编译器支持的方式,这两种方式可以通过在链接时指定–enable-new-dtags/–disable-new-dtags来控制。
总体的处理逻辑为:
if(对象没有RUNPATH) {
if(对象有RPATH){
使用RPATH
} else {
递归查找加载者(loader)的RPATH(或者有RUNPATH退出)
}

if(可执行程序没有RUNPATH) {
使用可执行程序的RPATH
}
}
查找LD_LIBRARY_PATH
查找正被加载对象的RUNPATH
查找ld.so.cache
查找默认路径

作者的编译器是最新的,默认就会使用RUNPATH。
从源码也可以看出,我们之前的验证是正确的。
使用–disable-new-dtags或旧式编译器的人可能会发现有时候-rpath优先于LD_LIBRARY_PATH,原因就在于程序进入了RUNPATH与RPATH的处理逻辑。可以使用readelf -d 查看动态节到底有没有RPATH或RUNPATH来进行分析

三:运行时加载顺序可能的风险分析
明白了这些运行时加载顺序,最后简单概括一下:
1:LD_LIBRARY_PATH的环境变量的影响范围是全局的,同LD_PRELOAD影响一样,会有风险点,过多的使用可能会影响到其他应用程序的运行,所以多用于调试模式。
2:-rpath链接选项是程序生成时指定的,一般程序运行前都已经生成了,所以这项暂时构成不了威胁。
3:ld.so.conf配置文件与LD_LIBRARY_PATH一样,都有同样的风险
4:/lib和/usr/lib 是系统文件,所属权限属于root,因为每个so库在各Linux系统中的位置有差异,要在这个位置预防so动态库劫持,就需要对库的位置进行精确定位,精准拦截。

从源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险

上一篇:如何在Ubuntu启动的时候自动加载内核模块?


下一篇:板载网卡MAC地址丢失后刷回方法[转]