动态编译的生成者
通常一个程序要想运行起来,除了依赖它自己之外,还需要ld.so和libc.so
动态库加载器可以通过运行一些动态链接的程序来间接调用(在这种情况中,不需要向动态库加载器指定任何命令行参数,对于 ELF 文件,动态库链接器的路径存储在将被执行的程序的 .interp section 中)或直接通过 /lib/ld-linux-so.* xx 来调用。
ld.so 与 ld-linux.so* 查找并加载程序依赖的动态库,为程序正常运行准备动态库环境,然后再运行程序
libc 是C语言标准函数库
ld 是动态链接器
共享库的版本
共享库的版本命名
Linux规定共享库的文件名规则必须如下: libname.so.x.y.z
前面是 lib + 库的名字 + .so , 后面跟是三个数字组成的版本号。
x 表示主版本号(Major Version Number)
y 表示次版本号(Minor Version Number)
z 表示发布版本号(Release Version Number)
- 主版本号表示库的重大升级,不同主版本号的库之间是不兼容的,依赖于旧的主版本号的程序需要改动相应的部分,并且重新编译,才可以在新版的共享库中运行;或者,系统必须保留旧版的共享库,使得那些依赖于旧版共享库的程序能够正常运行。
- 次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。
- 发布版本号表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行更改。相同主版本号、次版本号的共享库,不同的发布版本号之间完全兼容,依赖于某个发布版本号的程序可以在任何一个其他发布版本号中正常运行,而无须做任何修改
SO-NAME
什么是SO-NAME?
每个共享库都有一个对应的SO-NAME ,这个 SO-NAME 即共享库的文件名去掉次版本号和发布版本号,保留主版本号。比如一个共享库叫做 libfoo.so.2.6.1 ,那么它的 SO-NAME 即 libfoo.so.2 。
很明显,SO-NAME 规定了共享库的接口,SO-NAME 的两个相同共享库,次版本号大的兼容次版本号小的。在 Linux 系统中,统会为每个共享库在它所在的目录创建一个跟 SO-NAME 相同的并且指向它的软链接(SymbolLink)。比如系统中有存在一个共享库 /lib/libfoo.so.2.6.1 ,那么 Linux 中的共享库管理程序就会为它产生一个软链接 /lib/libfoo.so.2 指向它。
由于历史原因,动态链接器和 C 语言库的共享对象文件名规则不按 Linux 标准的共享库命名方法,但是C 语言的 SO-NAME 还是按照正常的规则。 另外动态连接器的 SO-NAME 命名不按照普通的规则。
SO-NAME的作用:
所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的 SO-NAME ,而不使用详细的版本号。动态链接文件中的 .dynamic 段中的 DT_NEED 类型的字段就是 SO-NAME 而不是共享库的完整名字,这样当动态链接器进行共享库依赖文件查找时,就会根据系统中各种共享库目录中的SO-NAME软链接自动定向到最新版本的共享库。当共享库进行升级的时候,如果只是进行增量升级,即保持主版本号不变,只改变次版本号或发布版本号,那么我们可以直接将新版的共享库替换掉旧版,并且修改 SO-NAME 的软链接指向新版本共享库,即可实现升级;当共享库的主版本号升级时,系统中就会存在多个 SO-NAME ,由于这些 SO-NAME 并不相同,所以已有的程序并不会受影响。
Linux 中提供了一个工具叫做 ldconfig ,当系统中安装或更新一个共享库时,就需要运行这个工具,它会遍历所有的默认共享库目录,比如 /lib 、 /usr/lib 等,然后更新所有的软链接,使它们指向最新版的共享库;如果安装了新的共享库,那么 ldconfig 会为其创建相应的软链接。
符号版本
根据提到的可知,一个程序所依赖的共享库的次版本号如果高于系统中的共享库,那么就不保证该程序能在该系统中运行,这类问题叫做次版本号交会问题(Minor-revision Rendezvous Problem)。 这种次版本号交会问题并没有因为 SO-NAME 的存在而得到任何改善。对于这个问题,现代的系统通过一种更加精巧的方式来解决,那就是符号版本机制。这个方案的基本思路是让每个导出和导入的符号都有一个相关联的版本号,它的实际做法类似于名称修饰的方法。 .dynamic 段中的 DT_VERSYM 类型字段包含了符号版本。它的作用是维护库的版本信息,以便在运行时进行版本控制和符号解析。通过 DT_VERSYM ,动态链接器可以确定所链接的库的版本与运行时环境是否兼容,以及选择正确的版本来解析符号。
共享库的系统路径
目前大多数包括 Linux 在内的开源操作系统都遵守一个叫做 FHS(File Hierarchy Standard)的标准,这个标准规定了一个系统中的系统文件应该如何存放,包括各个目录的结构、组织和作用,这有利于促进各个开源操作系统之间的兼容性。共享库作为系统中重要的文件,它们的存放方式也被 FHS 列入了规定范围。FHS 规定,一个系统中主要有两个存放共享库的位置,它们分别如下:
- /lib :该目录包含操作系统核心组件所需的共享库文件。这些库文件通常是系统引导和运行时所必需的,例如与操作系统内核相关的库文件。
- /usr/lib :该目录包含操作系统提供的额外共享库文件。这些库文件用于支持系统上安装的应用程序和工具的运行,如图形界面工具包(GUI toolkit)、网络库、数据库驱动程序等。
- /usr/local/lib :该目录是用于安装本地(local)软件的库文件的默认位置。当用户手动编译和安装软件到系统时,通常会将其安装到 /usr/local 目录下。因此,相关的库文件也会被安装到/usr/local/lib 目录下。
共享库的查找
动态链接器对于模块的查找有一定的规则:如果 DT_NEED 里面保存的是绝对路径,那么动态链接器就按照这个路径去查找;如果 DT_NEED 里面保存的是相对路径,那么动态链接器会在 /lib、 /usr/lib 和由 /etc/ld.so.conf 配置文件指定的目录中查找共享库。为了程序的可移植性和兼容性,共享库的路径往往是相对的。 ld.so.conf 是一个文本配置文件,它可能包含其他的配置文件,这些配置文件中存放着目录信息。
如果动态链接器在每次查找共享库时都去遍历这些目录,那将会非常耗费时间。所以 Linux 系统中都有一个叫做 ldconfig 的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME(即相应的符号链接),这样每个共享库的 SO-NAME 就能够指向正确的共享库文件;并且这个程序还会将这些 SO-NAME 收集起来,集中存放到 /etc/ld.so.cache 文件里面,并建立一个 SO-NAME 的缓存。当动态链接器要查找共享库时,它可以直接从 /etc/ld.so.cache 里面查找。而/etc/ld.so.cache 的结构是经过特殊设计的,非常适合查找,所以这个设计大大加快了共享库的查找过程。
如果动态链接器在 /etc/ld.so.cache 里面没有找到所需要的共享库,那么它还会遍历 /lib 和/usr/lib 这两个目录,如果还是没找到,就宣告失败。
所以理论上讲,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了 /etc/ld.so.conf 的配置,都应该运行 ldconfig 这个程序,以便调整 SO-NAME 和/etc/ld.so.cache 。很多软件包的安装程序在往系统里面安装共享库以后都会调用 ldconfig 。
共享库的更改
1.LD_LIBRARY_PATH
在 Linux 系统中, LD_LIBRARY_PATH 是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开。默认情况下, LD_LIBRARY_PATH 为空。如果我们为某个进程设置了 LD_LIBRARY_PATH ,那么进程在启动时,动态链接器在查找共享库时,会首先查找由 LD_LIBRARY_PATH 指定的目录。这个环境变量可以很方便地让我们测试新的共享库或使用非标准的共享库。
比如更换 libdl.so.2 和 libc.so.6 的 pwntools 脚本如下:
sh = process("./lib/ld.so --preload libdl.so.2 ./pwnhub".split(), env= {"LD_LIBRARY_PATH": "./lib/"})
2.LD_PRELOAD
系统中另外还有一个环境变量叫做 LD_PRELOAD ,这个文件中我们可以指定预先装载的一些共享库甚或是目标文件。在 LD_PRELOAD 里面指定的文件会在动态链接器按照固定规则搜索共享库之前装载,它比LD_LIBRARY_PATH 里面所指定的目录中的共享库还要优先。无论程序是否依赖于它们, LD_PRELOAD里面指定的共享库或目标文件都会被装载。
比如更换 libdl.so.2 和 libc.so.6 的 pwntools 脚本如下:
process("./lib/ld.so ./pwnhub".split(), env={"LD_PRELOAD": "./lib/libc.so.6 ./lib/libdl.so.2"})
3.LD_DEBUG
另外还有一个非常有用的环境变量 LD_DEBUG ,这个变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。 例如运行 LD_DEBUG=files /bin/ls 命令时动态链接器打印出了整个装载过程,显示程序依赖于哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。
- bindings :显示动态链接的符号绑定过程。
- libs :显示共享库的查找过程。
- versions :显示符号的版本依赖关系。
- reloc :显示重定位过程。
- symbols :显示符号表查找过程。
- statistics :显示动态链接过程中的各种统计信息。
patchelf
用于对于依赖不是很复杂的程序更换 libc ,有一下几点需要注意:
- 如果在漏洞利用时用到了动态链接相关结构最好不要 patchelf,因为 patchelf 会改变动态链接相关结构的位置。
- 一个程序在一个版本的虚拟机里面 patchelf 后换到另一个版本虚拟机中可能会运行失败。
- 在 patch 完 libc 后最好把 ld 也 patch 成大版本相同的 ld ,否则会运行失败。
- libc 和 ld 都需要有可执行权限。
修改libc:
patchelf --replace-needed libc.so.6 ./libc.so.6 ./pwn
修改ld:
patchelf --set-interpreter ./ld-2.31.so ./pwn