freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

源码分析:Linux共享库安全风险剖析 之 运行时加载顺序风险
2020-04-15 21:20:34

一:概述

       在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的软链接,

image001.png

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

image002.png


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

我们在这几个目录都放置好同名但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 

image003.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

image004.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

 

此时目录结构如下:

image005.png

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

image006.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_map *loader, const char *name,

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

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

 image007.pngimage008.pngimage009.pngimage010.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动态库劫持,就需要对库的位置进行精确定位,精准拦截。

 

 

 

# 安芯网盾
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者