Linux系统上的可执行文件/动态库

本文最后更新于2015年11月26日,已超过 1 年没有更新,如果文章内容失效,还请反馈给我,谢谢!

=Start=

缘由

当初学习《UNIX环境高级编程》的时候不认真,现在只好慢慢来还之前欠下的技术债。下面的内容主要摘抄自百度运维团队的博客文章:现代 *NIX 的进程与 shell,根据自己的学习和理解程度有部分删减。

内容

现在 *nix 系统的进程运行机制都比较类似,99.9% 的系统都已经使用 ELF 格式作为可执行文件格式,a.out 格式已经基本淘汰。此外 parser 的形式也被几乎所有系统支持,即文件头部写 #!/some/parser 的脚本。以下全文都以这两种情况展开,不考虑历史或者极特殊系统。

1.什么是可执行文件

一个文件可以被执行的意思是这个文件不需要外界驱动,而可以直接作为 execve(2) 的第一参数与之相反的就是不能被 execve(2) 直接作为第一参数,而必须在第二参数中作为参数传给第一参数指定的可执行文件。
对于 ELF 系统而言,只有 ELF 格式的文件是可执行的。一个 ELF 文件还需要在文件系统上具备执行权限,否则也无法作为第一参数。ELF 文件是否可以被执行取决于 ELF 文件自身是否是可执行的,例如 so 或者 detached debug symbols 也是 ELF,但是(99.9%)不能被执行。存在极少量特制的 so 可以被执行,例如 libc.so ,是特殊的 ELF 结构。ELF 的格式、结构不在本文的讨论当中。

大量非 ELF 格式的文件,例如脚本,是不能被操作系统载入并启动的。但是脚本文件可以作为 execve(2) 的第一参数,原因是 parser 体系。脚本解析器是解读脚本并且执行操作的 ELF 文件,例如常见的 shell 脚本,后缀名为 .sh 的,他们可以通过 sh script.sh 来运行。这个道理很简单,sh 是 ELF 文件,script.sh 被作为「数据」传递给了 sh 进行解读。为了方便这一类情况(这个情况占了可执行文件的大多数),操作系统规定了自动调用 parser 的格式:

#!/some/elf/file -param1 -param2
script line 1
script line 2

第一行顶格写 #! 表示这个文件需要 parser 支持,execve(2) 会去读取第一行的内容(为了防止溢出,通常第一行只会去读 80 个字节),然后把第一个字符串作为第一参数,重新传给 execve(2) 执行,第一行的其他参数会完整填充到 argv[1] 中,然后命令行上传来的 argv 被重新映射到新的位置。此时,由于 argv 数组的体积增加,有可能本来没有超过参数数组上限的命令现在超过了。这个过程不会递归,即如果 parser 字符串给出的不是 ELF 格式,那就不会再找下去了。

例如以下文件(a.awk):

shell 执行:

shell 调用:

操作系统发现 a.awk 需要 parser 操作系统调用:

此时的实际效果就变成了:

所谓的完整填充:

执行./a.out xxx yyy 会变成

即除了第一参数,后面所有的内容都是直接填进 argv[1] 的。

注意这个过程用户态可能可见(例如有些 shell 会手工完成这个过程而不是依靠操作系统),此外这个过程与扩展名无关,这与 DOS/Windows 不同(除非 shell 做了什么诡异的事),例如 sh a.awk 是完全合法的,如果 a.awk 内容其实是 shell 脚本。

当然,没有写 parser 头的文件也可以直接以 ELF 文件参数的形式启动,此时甚至不需要脚本文件自身有可执行权限,也不依赖 ${PATH}展开。 例如:

2.可执行文件的定位

*nix 系统没有“内部命令”的概念,这与 DOS/Windows 不同。例如平时执行的 ls,实际上是 /bin/ls,或者其他部署在磁盘上的文件。存在所谓的内部命令,例如 cd 就在磁盘上找不到,但是这个内部命令并不是由操作系统执行的,而是 shell 自己内部处理的,因此可以认为 *nix 的内部命令就是 shell 的内建命令。

对于一个磁盘上的可执行文件,操作系统需要给出其绝对路径,或者相对路径,但是不能没有路径。例如以下情况都是合法的:

但是以下是非法的:

不带路径的可执行文件需要由 shell 进行解析,解析结果会变成带路径的形式,操作系统才会接受,其依据一般为 ${PATH}环境变量。如果这个环境变量没有设置(不是为空),则 shell 通常会内部使用固定的值。这个值通常会包含 /bin 或者 /usr/bin 等常用 FHS 路径。对于大部分 shell 来说,这个值不一定在环境变量上得到体现,例如这样启动一个 bash:

此时可以通过 env 工具看出环境变量中没有 ${PATH},但是 set 内建命令会给出实际生效的值。和 DOS/Windows 不同的是,DOS/Windows 没有内部变量,一切都是环境变量,并且不管 %PATH% 取值如何,当前路径(即 .)一定是在生效路径的首位。

为了方便这类情况(也是占了大多数),libc 提供了若干 exec(3) 家族函数,用于 execve(2) 的封装。例如 execlp(3) 或者 execvp(3)。

3.动态库的定位

这一节的内容,有兴趣的可以去看一看关于 /lib/ld-linux.so.2 或者 /lib64/ld-linux-x86-64.so.2 的相关知识,以及其配置文件 /etc/ld.so.conf 外加环境变量 LD_LIBRARY_PATH 以及 LD_PRELOAD 等。熟练使用 ldd(1)、readelf(1) 以及 ld-linux.so.2 会给你很多深入的理解。

==

LD_LIBRARY_PATH
LD_PRELOAD

==

ldd
ldconfig
strace
/proc/$pid/maps
objdump
readelf
lsof
nm
strings
strip
pldd
pmap

==

参考链接

=EOF=

声明: 除非注明,ixyzero.com文章均为原创,转载请以链接形式标明本文地址,谢谢!
https://ixyzero.com/blog/archives/2560.html

《Linux系统上的可执行文件/动态库》上有11条评论

  1. 在Linux系统上有什么办法可以限制LD_PRELOAD 和 LD_LIBRARY_PATH这两个变量的使用么?
    http://security.stackexchange.com/questions/63599/is-there-any-way-to-block-ld-preload-and-ld-library-path-on-linux

    这个问题本质上,是需要你能完全控制应用的执行环境。没有什么魔法,我暂时想到的一些解决方案有:

    1. 将你关心的二进制文件添加 setuid/setgid 位。因为Linux正常情况下会阻止 setuid/setgid 程序的附加,所以请确认一下那个程序目前来说不属于root且设置了setuid位。
    2. 你可以使用一个更安全的装载程序来运行你的应用程序而不是ld,这相当于是拒绝承认LD_PRELOAD这样的变量。但这可能会破坏某些现有的应用程序。
    3. 在禁用LD_PRELOAD 和 dlsym机制的情况下用libc重新编译你的二进制程序。
    4. 在沙盒环境中运行你的应用,以阻止该应用使用自定义环境变量直接启动其它进程。

    选用什么解决方案具体取决于你需要运行什么程序,用户是谁,面临着哪些威胁。

    记住,恶意用户只能修改自己的执行环境(除非他可以提权至root)。所以,用户通常不使用LD_PRELOAD变量进行代码注入,因为他们已经有可运行代码的相同权限了。攻击一般会出现在一下几个场景中:

    1. 对于C/S模型的软件,在Client端突破一些安全相关的检查(欺骗通常出现在视频游戏中,或使客户端应用程序绕过一些分发服务器的检查);

    2. 当你接管了用户的会话或进程后,你可以用于安装恶意软件(比如:他们忘了注销计算机且你有对设备的物理访问权限 或者 你利用精心构造的内容获取了他们的应用程序的权限时);

  2. LD_PRELOAD的那些事
    https://www.zzsec.org/2013/04/something-about-ld_preload/

    动态库的搜索路径搜索的先后顺序是:
    编译目标代码时指定的动态库搜索路径
    环境变量LD_PRELOAD指定的动态库
    环境变量LD_LIBRARY_PATH指定的动态库搜索路径
    配置文件/etc/ld.so.conf中指定的动态库搜索路径
    默认的动态库搜索路径/lib
    默认的动态库搜索路径/usr/lib

    在上述1、3、4指定动态库搜索路径时,都可指定多个动态库搜索路径,其搜索的先后顺序是按指定路径的先后顺序搜索的。

  3. 静态库和动态库的优缺点 #详细
    http://chriszeng87.iteye.com/blog/1186094
    Linux动态库相关知识整理 #详细
    http://www.zkt.name/linuxgong-xiang-ku-de-chuang-jian-yu-shi-yong/
    Linux静态库和动态库 #详细
    http://www.cnblogs.com/feisky/archive/2010/03/09/1681996.html
    技巧:Linux 动态库与静态库制作及使用详解 #比较系统
    https://www.ibm.com/developerworks/cn/linux/l-cn-linklib/
    Linux下C调用静态库和动态库 #不错
    http://answerywj.com/2016/10/10/Linux%E4%B8%8BC%E8%B0%83%E7%94%A8%E9%9D%99%E6%80%81%E5%BA%93%E5%92%8C%E5%8A%A8%E6%80%81%E5%BA%93/
    Linux下编译链接动态库 #不错
    http://hbprotoss.github.io/posts/linuxxia-bian-yi-lian-jie-dong-tai-ku.html

    Linux下的静态库、动态库和动态加载库
    http://www.techug.com/linux-static-lib-dynamic-lib
    Linux下动态库(.so)和静态库(.a)
    http://blog.csdn.net/felixit0120/article/details/7652907
    Linux系统中“动态库”和“静态库”那点事儿
    http://blog.jobbole.com/107977/
    静态库和动态库
    http://leanote.com/blog/post/57907b2cab644133ed01bbf3
    静态库与动态库的使用
    https://www.gitbook.com/book/leon_lizi/-framework-/details
    Linux下静态、动态库(隐式、显式调用)的创建和使用及区别
    http://blog.csdn.net/star_xiong/article/details/17301191

  4. 用strace进行问题定位
    https://stackoverflow.com/questions/174942/how-should-strace-be-used
    http://www.thegeekstuff.com/2011/11/strace-examples
    https://linoxide.com/linux-command/linux-strace-command-examples/

    $ strace ls
    $ strace -e open cat /etc/passwd #只追踪特定的系统调用
    $ strace -e trace=open,read ls /etc/ #同时追踪多个系统调用
    $ sudo strace -p 1846 #对由pid指定的进程进行分析
    $ sudo strace -o process_strace.log -p 1846 #保存输出至文件
    $ strace -c ls #显示最终的统计结果
    $ strace -t ls #显示时间戳
    $ strace -tt ls #显示时间戳(微秒)
    $ strace -r ls #显示相对时间

发表评论

电子邮件地址不会被公开。 必填项已用*标注