linux安全源码,从源码分析: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的软链接,

f621e0d82c816fd12de72e4474a73d69.png

所以,我们只测试前四种情况+PATH+本地!

65dcaee677297636387a3e652bca704e.png

为了公平验证所有运行时加载顺序,

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

编写七个文件如下

user@kali:~/pro$ cat a1.c

#include

void myprint()

{

printf("Hello a1[LD_LIBRARY_PATH]\n");

}

user@kali:~/pro$ cat a2.c

#include

void myprint()

{

printf("Hello a2[rpath]\n");

}

user@kali:~/pro$ cat a3.c

#include

void myprint()

{

printf("Hello a3[ld.so.conf]\n");

}

user@kali:~/pro$ cat a4.c

#include

void myprint()

{

printf("Hello a4[/lib--/usr/lib]\n");

}

user@kali:~/pro$ cat a5.c

#include

void myprint()

{

printf("Hello a5 PATH\n");

}

user@kali:~/pro$ cat a6.c

#include

void myprint()

{

printf("Hello a6 local\n");

}

user@kali:~/pro$ cat main.c

#include

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

c3682a2d60abbc3e3f242950281fb17e.png

// 刷新缓存

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

5ee68fc4bc41b07420542f9715287da9.png

// 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

此时目录结构如下:

c8213664a919833fc3a96d193b794c4a.png

我们看一下动态节.dynamic的内容:

b318bfa373fae7f34d5f81d4c756d25a.png

可见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_maploader, const char *name,

int type, int trace_mode, int mode, Lmid_t nsid)

几个主要的代码片段如下:

616549a4411f578dcbef8fcc8e1b97a6.png

a4859b183d12c56ec809a16b4c351a0d.png

d1dd93202a5ba7e762fda9f087ece08f.png

74df8fdaba42be63997a2ca62941cfea.png

最开始是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动态库劫持,就需要对库的位置进行精确定位,精准拦截。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值